mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-29 16:14:44 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0316813792 | ||
|
|
d201a5396c | ||
|
|
86f9396b6c | ||
|
|
0fb4bb3cb0 | ||
|
|
1528d7ce25 | ||
|
|
4e7eeb7989 | ||
|
|
7f2976a4f4 | ||
|
|
8928cb92da | ||
|
|
a181b76124 | ||
|
|
8f085a8f63 | ||
|
|
93a997351b | ||
|
|
b3af6c1c15 | ||
|
|
4a65263f7d | ||
|
|
1091b5b9b4 | ||
|
|
f6b3ff2f5c | ||
|
|
b399bdf5cf | ||
|
|
9ace161bc8 |
BIN
Read.png
BIN
Read.png
Binary file not shown.
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 162 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
|
||||||
|
MANGA_DATE_PREFIX_DEFAULT = "" # Default for the new prefix
|
||||||
STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering" # For manga post title + global counter
|
STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering" # For manga post title + global counter
|
||||||
|
|
||||||
SKIP_SCOPE_FILES = "files"
|
SKIP_SCOPE_FILES = "files"
|
||||||
@@ -51,6 +52,9 @@ KNOWN_NAMES = [] # This will now store dicts: {'name': str, 'is_group': bool, 'a
|
|||||||
|
|
||||||
MIN_SIZE_FOR_MULTIPART_DOWNLOAD = 10 * 1024 * 1024 # 10 MB - Stays the same
|
MIN_SIZE_FOR_MULTIPART_DOWNLOAD = 10 * 1024 * 1024 # 10 MB - Stays the same
|
||||||
MAX_PARTS_FOR_MULTIPART_DOWNLOAD = 15 # Max concurrent connections for a single file
|
MAX_PARTS_FOR_MULTIPART_DOWNLOAD = 15 # Max concurrent connections for a single file
|
||||||
|
# Max length for a single filename or folder name component to ensure cross-OS compatibility
|
||||||
|
# Windows MAX_PATH is 260 for the full path. Individual components are usually shorter.
|
||||||
|
MAX_FILENAME_COMPONENT_LENGTH = 150
|
||||||
|
|
||||||
IMAGE_EXTENSIONS = {
|
IMAGE_EXTENSIONS = {
|
||||||
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp',
|
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp',
|
||||||
@@ -63,6 +67,11 @@ VIDEO_EXTENSIONS = {
|
|||||||
ARCHIVE_EXTENSIONS = {
|
ARCHIVE_EXTENSIONS = {
|
||||||
'.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'
|
'.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'
|
||||||
}
|
}
|
||||||
|
AUDIO_EXTENSIONS = {
|
||||||
|
'.mp3', '.wav', '.aac', '.flac', '.ogg', '.wma', '.m4a', '.opus',
|
||||||
|
'.aiff', '.ape', '.mid', '.midi'
|
||||||
|
}
|
||||||
|
|
||||||
def parse_cookie_string(cookie_string):
|
def parse_cookie_string(cookie_string):
|
||||||
"""Parses a 'name=value; name2=value2' cookie string into a dict."""
|
"""Parses a 'name=value; name2=value2' cookie string into a dict."""
|
||||||
cookies = {}
|
cookies = {}
|
||||||
@@ -131,18 +140,46 @@ def clean_folder_name(name):
|
|||||||
|
|
||||||
if not cleaned: # If empty after initial cleaning
|
if not cleaned: # If empty after initial cleaning
|
||||||
return "untitled_folder"
|
return "untitled_folder"
|
||||||
|
|
||||||
|
# Truncate if too long
|
||||||
|
if len(cleaned) > MAX_FILENAME_COMPONENT_LENGTH:
|
||||||
|
cleaned = cleaned[:MAX_FILENAME_COMPONENT_LENGTH]
|
||||||
|
# After truncation, it's possible a new trailing space/dot is at the end
|
||||||
|
# or an existing one remains. So, strip them using the loop below.
|
||||||
|
|
||||||
|
# Strip trailing dots/spaces (original logic, now applied to potentially truncated name)
|
||||||
temp_name = cleaned
|
temp_name = cleaned
|
||||||
while len(temp_name) > 0 and (temp_name.endswith('.') or temp_name.endswith(' ')):
|
while len(temp_name) > 0 and (temp_name.endswith('.') or temp_name.endswith(' ')):
|
||||||
temp_name = temp_name[:-1]
|
temp_name = temp_name[:-1]
|
||||||
|
|
||||||
return temp_name if temp_name else "untitled_folder"
|
return temp_name if temp_name else "untitled_folder"
|
||||||
|
|
||||||
|
|
||||||
def clean_filename(name):
|
def clean_filename(name):
|
||||||
if not isinstance(name, str): name = str(name)
|
if not isinstance(name, str): name = str(name)
|
||||||
cleaned = re.sub(r'[^\w\s\-\_\.\(\)]', '', name)
|
cleaned = re.sub(r'[^\w\s\-\_\.\(\)]', '', name)
|
||||||
cleaned = cleaned.strip()
|
cleaned = cleaned.strip() # Remove leading/trailing spaces first
|
||||||
cleaned = re.sub(r'\s+', '_', cleaned)
|
cleaned = re.sub(r'\s+', ' ', cleaned) # Replace multiple internal spaces with a single space
|
||||||
return cleaned if cleaned else "untitled_file"
|
|
||||||
|
if not cleaned: return "untitled_file"
|
||||||
|
|
||||||
|
base_name, ext = os.path.splitext(cleaned)
|
||||||
|
|
||||||
|
# Calculate max length for base_name, reserving space for the extension
|
||||||
|
max_base_len = MAX_FILENAME_COMPONENT_LENGTH - len(ext)
|
||||||
|
|
||||||
|
if len(base_name) > max_base_len:
|
||||||
|
if max_base_len > 0: # If there's space for at least some of the base name
|
||||||
|
base_name = base_name[:max_base_len]
|
||||||
|
else: # No space for base name (extension is too long or fills the entire allowed space)
|
||||||
|
# In this case, we have to truncate the original 'cleaned' string,
|
||||||
|
# which might cut into the extension, but it's necessary to meet the length.
|
||||||
|
return cleaned[:MAX_FILENAME_COMPONENT_LENGTH] if cleaned else "untitled_file"
|
||||||
|
|
||||||
|
final_name = base_name + ext
|
||||||
|
# Ensure the final reconstructed name isn't empty (e.g. if base_name became empty and ext was also empty)
|
||||||
|
return final_name if final_name else "untitled_file"
|
||||||
|
|
||||||
|
|
||||||
def strip_html_tags(html_text):
|
def strip_html_tags(html_text):
|
||||||
if not html_text: return ""
|
if not html_text: return ""
|
||||||
@@ -218,6 +255,12 @@ def is_archive(filename):
|
|||||||
_, ext = os.path.splitext(filename)
|
_, ext = os.path.splitext(filename)
|
||||||
return ext.lower() in ARCHIVE_EXTENSIONS
|
return ext.lower() in ARCHIVE_EXTENSIONS
|
||||||
|
|
||||||
|
def is_audio(filename):
|
||||||
|
if not filename: return False
|
||||||
|
_, ext = os.path.splitext(filename)
|
||||||
|
return ext.lower() in AUDIO_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def is_post_url(url):
|
def is_post_url(url):
|
||||||
if not isinstance(url, str): return False
|
if not isinstance(url, str): return False
|
||||||
@@ -604,7 +647,9 @@ class PostProcessorWorker:
|
|||||||
use_cookie=False, # Added missing parameter
|
use_cookie=False, # Added missing parameter
|
||||||
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_prefix=MANGA_DATE_PREFIX_DEFAULT, # New parameter for date-based prefix
|
||||||
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
|
||||||
|
scan_content_for_images=False, # New flag for scanning HTML content
|
||||||
manga_global_file_counter_ref=None, # New parameter for global numbering
|
manga_global_file_counter_ref=None, # New parameter for global numbering
|
||||||
): # type: ignore
|
): # type: ignore
|
||||||
self.post = post_data
|
self.post = post_data
|
||||||
@@ -652,8 +697,10 @@ 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_date_prefix = manga_date_prefix # Store the prefix
|
||||||
self.manga_global_file_counter_ref = manga_global_file_counter_ref # Store global counter
|
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
|
||||||
|
self.scan_content_for_images = scan_content_for_images # Store new flag
|
||||||
|
|
||||||
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.")
|
self.logger("⚠️ Image compression disabled: Pillow library not found.")
|
||||||
@@ -734,6 +781,14 @@ class PostProcessorWorker:
|
|||||||
if self.manga_mode_active: # Note: duplicate_file_mode is overridden to "Delete" in main.py if manga_mode is on
|
if self.manga_mode_active: # Note: duplicate_file_mode is overridden to "Delete" in main.py if manga_mode is on
|
||||||
if self.manga_filename_style == STYLE_ORIGINAL_NAME:
|
if self.manga_filename_style == STYLE_ORIGINAL_NAME:
|
||||||
filename_to_save_in_main_path = clean_filename(api_original_filename)
|
filename_to_save_in_main_path = clean_filename(api_original_filename)
|
||||||
|
# Apply prefix if provided for Original Name style
|
||||||
|
if self.manga_date_prefix and self.manga_date_prefix.strip():
|
||||||
|
cleaned_prefix = clean_filename(self.manga_date_prefix.strip())
|
||||||
|
if cleaned_prefix:
|
||||||
|
filename_to_save_in_main_path = f"{cleaned_prefix} {filename_to_save_in_main_path}"
|
||||||
|
else:
|
||||||
|
self.logger(f"⚠️ Manga Original Name Mode: Provided prefix '{self.manga_date_prefix}' was empty after cleaning. Using original name only.")
|
||||||
|
|
||||||
was_original_name_kept_flag = True
|
was_original_name_kept_flag = True
|
||||||
elif self.manga_filename_style == STYLE_POST_TITLE:
|
elif self.manga_filename_style == STYLE_POST_TITLE:
|
||||||
if post_title and post_title.strip():
|
if post_title and post_title.strip():
|
||||||
@@ -742,8 +797,8 @@ class PostProcessorWorker:
|
|||||||
if file_index_in_post == 0:
|
if file_index_in_post == 0:
|
||||||
filename_to_save_in_main_path = f"{cleaned_post_title_base}{original_ext}"
|
filename_to_save_in_main_path = f"{cleaned_post_title_base}{original_ext}"
|
||||||
else:
|
else:
|
||||||
filename_to_save_in_main_path = clean_filename(api_original_filename)
|
filename_to_save_in_main_path = f"{cleaned_post_title_base}_{file_index_in_post}{original_ext}"
|
||||||
was_original_name_kept_flag = True
|
was_original_name_kept_flag = False # Name is derived, not original
|
||||||
else:
|
else:
|
||||||
filename_to_save_in_main_path = f"{cleaned_post_title_base}{original_ext}"
|
filename_to_save_in_main_path = f"{cleaned_post_title_base}{original_ext}"
|
||||||
else:
|
else:
|
||||||
@@ -759,7 +814,15 @@ class PostProcessorWorker:
|
|||||||
counter_val_for_filename = manga_date_file_counter_ref[0]
|
counter_val_for_filename = manga_date_file_counter_ref[0]
|
||||||
manga_date_file_counter_ref[0] += 1
|
manga_date_file_counter_ref[0] += 1
|
||||||
|
|
||||||
filename_to_save_in_main_path = f"{counter_val_for_filename:03d}{original_ext}"
|
base_numbered_name = f"{counter_val_for_filename:03d}"
|
||||||
|
if self.manga_date_prefix and self.manga_date_prefix.strip():
|
||||||
|
cleaned_prefix = clean_filename(self.manga_date_prefix.strip())
|
||||||
|
if cleaned_prefix: # Ensure prefix is not empty after cleaning
|
||||||
|
filename_to_save_in_main_path = f"{cleaned_prefix} {base_numbered_name}{original_ext}"
|
||||||
|
else: # Prefix became empty after cleaning
|
||||||
|
filename_to_save_in_main_path = f"{base_numbered_name}{original_ext}"; self.logger(f"⚠️ Manga Date Mode: Provided prefix '{self.manga_date_prefix}' was empty after cleaning. Using number only.")
|
||||||
|
else: # No prefix provided
|
||||||
|
filename_to_save_in_main_path = f"{base_numbered_name}{original_ext}"
|
||||||
else:
|
else:
|
||||||
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)
|
||||||
@@ -796,8 +859,10 @@ class PostProcessorWorker:
|
|||||||
if not word_to_remove: continue
|
if not word_to_remove: continue
|
||||||
pattern = re.compile(re.escape(word_to_remove), re.IGNORECASE)
|
pattern = re.compile(re.escape(word_to_remove), re.IGNORECASE)
|
||||||
modified_base_name = pattern.sub("", modified_base_name)
|
modified_base_name = pattern.sub("", modified_base_name)
|
||||||
modified_base_name = re.sub(r'[_.\s-]+', '_', modified_base_name)
|
# After removals, normalize all seps (underscore, dot, multiple spaces, hyphen) to a single space, then strip.
|
||||||
modified_base_name = modified_base_name.strip('_')
|
modified_base_name = re.sub(r'[_.\s-]+', ' ', modified_base_name) # Convert all separators to spaces
|
||||||
|
modified_base_name = re.sub(r'\s+', ' ', modified_base_name) # Condense multiple spaces to one
|
||||||
|
modified_base_name = modified_base_name.strip() # Remove leading/trailing spaces
|
||||||
if modified_base_name and modified_base_name != ext_for_removal.lstrip('.'):
|
if modified_base_name and modified_base_name != ext_for_removal.lstrip('.'):
|
||||||
filename_to_save_in_main_path = modified_base_name + ext_for_removal
|
filename_to_save_in_main_path = modified_base_name + ext_for_removal
|
||||||
else:
|
else:
|
||||||
@@ -807,6 +872,7 @@ class PostProcessorWorker:
|
|||||||
is_img_type = is_image(api_original_filename)
|
is_img_type = is_image(api_original_filename)
|
||||||
is_vid_type = is_video(api_original_filename)
|
is_vid_type = is_video(api_original_filename)
|
||||||
is_archive_type = is_archive(api_original_filename)
|
is_archive_type = is_archive(api_original_filename)
|
||||||
|
is_audio_type = is_audio(api_original_filename)
|
||||||
|
|
||||||
if self.filter_mode == 'archive':
|
if self.filter_mode == 'archive':
|
||||||
if not is_archive_type:
|
if not is_archive_type:
|
||||||
@@ -820,6 +886,10 @@ class PostProcessorWorker:
|
|||||||
if not is_vid_type:
|
if not is_vid_type:
|
||||||
self.logger(f" -> Filter Skip: '{api_original_filename}' (Not Video).")
|
self.logger(f" -> Filter Skip: '{api_original_filename}' (Not Video).")
|
||||||
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
||||||
|
elif self.filter_mode == 'audio': # New audio filter mode
|
||||||
|
if not is_audio_type:
|
||||||
|
self.logger(f" -> Filter Skip: '{api_original_filename}' (Not Audio).")
|
||||||
|
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
||||||
|
|
||||||
if self.skip_zip and is_zip(api_original_filename):
|
if self.skip_zip and is_zip(api_original_filename):
|
||||||
self.logger(f" -> Pref Skip: '{api_original_filename}' (ZIP).")
|
self.logger(f" -> Pref Skip: '{api_original_filename}' (ZIP).")
|
||||||
@@ -1318,14 +1388,14 @@ class PostProcessorWorker:
|
|||||||
if original_api_name:
|
if original_api_name:
|
||||||
all_files_from_post_api.append({
|
all_files_from_post_api.append({
|
||||||
'url': f"https://{api_file_domain}{file_path}" if file_path.startswith('/') else f"https://{api_file_domain}/data/{file_path}",
|
'url': f"https://{api_file_domain}{file_path}" if file_path.startswith('/') else f"https://{api_file_domain}/data/{file_path}",
|
||||||
'name': original_api_name,
|
'name': original_api_name, # This is the cleaned/API provided name
|
||||||
'_original_name_for_log': original_api_name,
|
'_original_name_for_log': original_api_name,
|
||||||
'_is_thumbnail': self.download_thumbnails and is_image(original_api_name)
|
'_is_thumbnail': is_image(original_api_name) # Mark if it's an image from API
|
||||||
})
|
})
|
||||||
else: self.logger(f" ⚠️ Skipping main file for post {post_id}: Missing name (Path: {file_path})")
|
else: self.logger(f" ⚠️ Skipping main file for post {post_id}: Missing name (Path: {file_path})")
|
||||||
|
|
||||||
for idx, att_info in enumerate(post_attachments):
|
for idx, att_info in enumerate(post_attachments):
|
||||||
if isinstance(att_info, dict) and att_info.get('path'):
|
if isinstance(att_info, dict) and att_info.get('path'): # Ensure att_info is a dict
|
||||||
att_path = att_info['path'].lstrip('/')
|
att_path = att_info['path'].lstrip('/')
|
||||||
original_api_att_name = att_info.get('name') or os.path.basename(att_path)
|
original_api_att_name = att_info.get('name') or os.path.basename(att_path)
|
||||||
if original_api_att_name:
|
if original_api_att_name:
|
||||||
@@ -1333,16 +1403,99 @@ class PostProcessorWorker:
|
|||||||
'url': f"https://{api_file_domain}{att_path}" if att_path.startswith('/') else f"https://{api_file_domain}/data/{att_path}",
|
'url': f"https://{api_file_domain}{att_path}" if att_path.startswith('/') else f"https://{api_file_domain}/data/{att_path}",
|
||||||
'name': original_api_att_name,
|
'name': original_api_att_name,
|
||||||
'_original_name_for_log': original_api_att_name,
|
'_original_name_for_log': original_api_att_name,
|
||||||
'_is_thumbnail': self.download_thumbnails and is_image(original_api_att_name)
|
'_is_thumbnail': is_image(original_api_att_name) # Mark if it's an image from API
|
||||||
})
|
})
|
||||||
else: self.logger(f" ⚠️ Skipping attachment {idx+1} for post {post_id}: Missing name (Path: {att_path})")
|
else: self.logger(f" ⚠️ Skipping attachment {idx+1} for post {post_id}: Missing name (Path: {att_path})")
|
||||||
else: self.logger(f" ⚠️ Skipping invalid attachment {idx+1} for post {post_id}: {str(att_info)[:100]}")
|
else: self.logger(f" ⚠️ Skipping invalid attachment {idx+1} for post {post_id}: {str(att_info)[:100]}")
|
||||||
|
|
||||||
|
# --- New: Scan post content for additional image URLs if enabled ---
|
||||||
|
if self.scan_content_for_images and post_content_html and not self.extract_links_only: # This block was duplicated, ensure only one exists
|
||||||
|
self.logger(f" Scanning post content for additional image URLs (Post ID: {post_id})...")
|
||||||
|
|
||||||
|
parsed_input_url = urlparse(self.api_url_input)
|
||||||
|
base_url_for_relative_paths = f"{parsed_input_url.scheme}://{parsed_input_url.netloc}"
|
||||||
|
img_ext_pattern = "|".join(ext.lstrip('.') for ext in IMAGE_EXTENSIONS)
|
||||||
|
|
||||||
|
# 1. Regex for direct absolute image URLs in text
|
||||||
|
direct_url_pattern_str = r"""(?i)\b(https?://[^\s"'<>\[\]\{\}\|\^\\^~\[\]`]+\.(?:""" + img_ext_pattern + r"""))\b"""
|
||||||
|
# 2. Regex for <img> tags (captures src content)
|
||||||
|
img_tag_src_pattern_str = r"""<img\s+[^>]*?src\s*=\s*["']([^"']+)["']"""
|
||||||
|
|
||||||
|
found_image_sources = set()
|
||||||
|
|
||||||
|
for direct_url_match in re.finditer(direct_url_pattern_str, post_content_html):
|
||||||
|
found_image_sources.add(direct_url_match.group(1))
|
||||||
|
|
||||||
|
for img_tag_match in re.finditer(img_tag_src_pattern_str, post_content_html, re.IGNORECASE):
|
||||||
|
src_attr = img_tag_match.group(1).strip()
|
||||||
|
src_attr = html.unescape(src_attr)
|
||||||
|
if not src_attr: continue
|
||||||
|
|
||||||
|
resolved_src_url = ""
|
||||||
|
if src_attr.startswith(('http://', 'https://')):
|
||||||
|
resolved_src_url = src_attr
|
||||||
|
elif src_attr.startswith('//'):
|
||||||
|
resolved_src_url = f"{parsed_input_url.scheme}:{src_attr}"
|
||||||
|
elif src_attr.startswith('/'):
|
||||||
|
resolved_src_url = f"{base_url_for_relative_paths}{src_attr}"
|
||||||
|
|
||||||
|
if resolved_src_url:
|
||||||
|
parsed_resolved_url = urlparse(resolved_src_url)
|
||||||
|
if any(parsed_resolved_url.path.lower().endswith(ext) for ext in IMAGE_EXTENSIONS):
|
||||||
|
found_image_sources.add(resolved_src_url)
|
||||||
|
|
||||||
|
if found_image_sources:
|
||||||
|
self.logger(f" Found {len(found_image_sources)} potential image URLs/sources in content.")
|
||||||
|
existing_urls_in_api_list = {f_info['url'] for f_info in all_files_from_post_api}
|
||||||
|
|
||||||
|
for found_url in found_image_sources: # Iterate over the unique, resolved URLs
|
||||||
|
if self.check_cancel(): break
|
||||||
|
if found_url in existing_urls_in_api_list:
|
||||||
|
self.logger(f" Skipping URL from content (already in API list or previously added from content): {found_url[:70]}...")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
parsed_found_url = urlparse(found_url)
|
||||||
|
url_filename = os.path.basename(parsed_found_url.path)
|
||||||
|
if not url_filename or not is_image(url_filename):
|
||||||
|
self.logger(f" Skipping URL from content (no filename part or not an image extension): {found_url[:70]}...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.logger(f" Adding image from content: {url_filename} (URL: {found_url[:70]}...)")
|
||||||
|
all_files_from_post_api.append({
|
||||||
|
'url': found_url,
|
||||||
|
'name': url_filename,
|
||||||
|
'_original_name_for_log': url_filename,
|
||||||
|
'_is_thumbnail': False, # Images from content are not API thumbnails
|
||||||
|
'_from_content_scan': True
|
||||||
|
})
|
||||||
|
existing_urls_in_api_list.add(found_url)
|
||||||
|
except Exception as e_url_parse:
|
||||||
|
self.logger(f" Error processing URL from content '{found_url[:70]}...': {e_url_parse}")
|
||||||
|
else:
|
||||||
|
self.logger(f" No additional image URLs found in post content scan for post {post_id}.")
|
||||||
|
# --- End of new content scanning logic ---
|
||||||
|
|
||||||
|
# --- Final filtering based on download_thumbnails and scan_content_for_images flags ---
|
||||||
if self.download_thumbnails:
|
if self.download_thumbnails:
|
||||||
all_files_from_post_api = [finfo for finfo in all_files_from_post_api if finfo['_is_thumbnail']]
|
if self.scan_content_for_images:
|
||||||
if not all_files_from_post_api:
|
# Both "Download Thumbnails Only" AND "Scan Content for Images" are checked.
|
||||||
self.logger(f" -> No image thumbnails found for post {post_id} in thumbnail-only mode.")
|
# Prioritize images from content scan.
|
||||||
return 0, 0, [], []
|
self.logger(f" Mode: 'Download Thumbnails Only' + 'Scan Content for Images' active. Prioritizing images from content scan for post {post_id}.")
|
||||||
|
all_files_from_post_api = [finfo for finfo in all_files_from_post_api if finfo.get('_from_content_scan')]
|
||||||
|
if not all_files_from_post_api:
|
||||||
|
self.logger(f" -> No images found via content scan for post {post_id} in this combined mode.")
|
||||||
|
return 0, 0, [], [] # No files to download for this post
|
||||||
|
else:
|
||||||
|
# Only "Download Thumbnails Only" is checked. Filter for API thumbnails.
|
||||||
|
self.logger(f" Mode: 'Download Thumbnails Only' active. Filtering for API thumbnails for post {post_id}.")
|
||||||
|
all_files_from_post_api = [finfo for finfo in all_files_from_post_api if finfo.get('_is_thumbnail')]
|
||||||
|
if not all_files_from_post_api:
|
||||||
|
self.logger(f" -> No API image thumbnails found for post {post_id} in thumbnail-only mode.")
|
||||||
|
return 0, 0, [], [] # No files to download for this post
|
||||||
|
# If self.download_thumbnails is False, all_files_from_post_api remains as is.
|
||||||
|
# It will contain all API files (images marked with _is_thumbnail: True, others False)
|
||||||
|
# and potentially content-scanned images (marked with _from_content_scan: True).
|
||||||
|
|
||||||
if self.manga_mode_active and self.manga_filename_style == STYLE_DATE_BASED:
|
if self.manga_mode_active and self.manga_filename_style == STYLE_DATE_BASED:
|
||||||
def natural_sort_key_for_files(file_api_info):
|
def natural_sort_key_for_files(file_api_info):
|
||||||
name = file_api_info.get('_original_name_for_log', '').lower()
|
name = file_api_info.get('_original_name_for_log', '').lower()
|
||||||
@@ -1548,12 +1701,14 @@ class DownloadThread(QThread):
|
|||||||
manga_filename_style=STYLE_POST_TITLE,
|
manga_filename_style=STYLE_POST_TITLE,
|
||||||
char_filter_scope=CHAR_SCOPE_FILES, # manga_date_file_counter_ref removed from here
|
char_filter_scope=CHAR_SCOPE_FILES, # manga_date_file_counter_ref removed from here
|
||||||
remove_from_filename_words_list=None,
|
remove_from_filename_words_list=None,
|
||||||
|
manga_date_prefix=MANGA_DATE_PREFIX_DEFAULT, # New parameter
|
||||||
allow_multipart_download=True,
|
allow_multipart_download=True,
|
||||||
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
|
manga_global_file_counter_ref=None, # New parameter for global numbering
|
||||||
use_cookie=False, # Added: Expected by main.py
|
use_cookie=False, # Added: Expected by main.py
|
||||||
|
scan_content_for_images=False, # Added new flag
|
||||||
cookie_text="", # Added: Expected by main.py
|
cookie_text="", # Added: Expected by main.py
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -1597,12 +1752,14 @@ class DownloadThread(QThread):
|
|||||||
self.manga_filename_style = manga_filename_style
|
self.manga_filename_style = manga_filename_style
|
||||||
self.char_filter_scope = char_filter_scope
|
self.char_filter_scope = char_filter_scope
|
||||||
self.remove_from_filename_words_list = remove_from_filename_words_list
|
self.remove_from_filename_words_list = remove_from_filename_words_list
|
||||||
|
self.manga_date_prefix = manga_date_prefix # Store the prefix
|
||||||
self.allow_multipart_download = allow_multipart_download
|
self.allow_multipart_download = allow_multipart_download
|
||||||
self.selected_cookie_file = selected_cookie_file # Store selected cookie file
|
self.selected_cookie_file = selected_cookie_file # Store selected cookie file
|
||||||
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.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.scan_content_for_images = scan_content_for_images # Store new flag
|
||||||
self.manga_global_file_counter_ref = manga_global_file_counter_ref # Store for global numbering
|
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).")
|
||||||
@@ -1726,6 +1883,7 @@ class DownloadThread(QThread):
|
|||||||
skip_current_file_flag=self.skip_current_file_flag,
|
skip_current_file_flag=self.skip_current_file_flag,
|
||||||
manga_mode_active=self.manga_mode_active,
|
manga_mode_active=self.manga_mode_active,
|
||||||
manga_filename_style=self.manga_filename_style,
|
manga_filename_style=self.manga_filename_style,
|
||||||
|
manga_date_prefix=self.manga_date_prefix, # Pass the prefix
|
||||||
char_filter_scope=self.char_filter_scope,
|
char_filter_scope=self.char_filter_scope,
|
||||||
remove_from_filename_words_list=self.remove_from_filename_words_list,
|
remove_from_filename_words_list=self.remove_from_filename_words_list,
|
||||||
allow_multipart_download=self.allow_multipart_download,
|
allow_multipart_download=self.allow_multipart_download,
|
||||||
@@ -1735,6 +1893,7 @@ class DownloadThread(QThread):
|
|||||||
manga_global_file_counter_ref=self.manga_global_file_counter_ref, # Pass the ref
|
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
|
||||||
|
scan_content_for_images=self.scan_content_for_images, # Pass new flag
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
dl_count, skip_count, kept_originals_this_post, retryable_failures = post_processing_worker.process()
|
dl_count, skip_count, kept_originals_this_post, retryable_failures = post_processing_worker.process()
|
||||||
|
|||||||
388
main.py
388
main.py
@@ -54,8 +54,10 @@ 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,
|
||||||
|
STYLE_DATE_BASED, # Import new manga style
|
||||||
STYLE_POST_TITLE_GLOBAL_NUMBERING # Import new manga style
|
STYLE_POST_TITLE_GLOBAL_NUMBERING # Import new manga style
|
||||||
|
# IMAGE_EXTENSIONS will be used from downloader_utils directly
|
||||||
)
|
)
|
||||||
print("Successfully imported names from downloader_utils.")
|
print("Successfully imported names from downloader_utils.")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
@@ -88,6 +90,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_DATE_BASED = "date_based"
|
||||||
STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering" # Mock for safety
|
STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering" # Mock for safety
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -112,7 +115,7 @@ HTML_PREFIX = "<!HTML!>"
|
|||||||
CONFIG_ORGANIZATION_NAME = "KemonoDownloader"
|
CONFIG_ORGANIZATION_NAME = "KemonoDownloader"
|
||||||
CONFIG_APP_NAME_MAIN = "ApplicationSettings"
|
CONFIG_APP_NAME_MAIN = "ApplicationSettings"
|
||||||
MANGA_FILENAME_STYLE_KEY = "mangaFilenameStyleV1"
|
MANGA_FILENAME_STYLE_KEY = "mangaFilenameStyleV1"
|
||||||
STYLE_POST_TITLE = "post_title"
|
STYLE_POST_TITLE = "post_title" # Already defined, but ensure it's STYLE_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
|
STYLE_POST_TITLE_GLOBAL_NUMBERING = STYLE_POST_TITLE_GLOBAL_NUMBERING # Use imported or mocked
|
||||||
@@ -122,6 +125,7 @@ ALLOW_MULTIPART_DOWNLOAD_KEY = "allowMultipartDownloadV1"
|
|||||||
USE_COOKIE_KEY = "useCookieV1" # New setting key
|
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"
|
||||||
|
SCAN_CONTENT_IMAGES_KEY = "scanContentForImagesV1" # New setting key
|
||||||
|
|
||||||
CONFIRM_ADD_ALL_ACCEPTED = 1
|
CONFIRM_ADD_ALL_ACCEPTED = 1
|
||||||
CONFIRM_ADD_ALL_SKIP_ADDING = 2
|
CONFIRM_ADD_ALL_SKIP_ADDING = 2
|
||||||
@@ -233,6 +237,97 @@ class ConfirmAddAllDialog(QDialog):
|
|||||||
return CONFIRM_ADD_ALL_SKIP_ADDING
|
return CONFIRM_ADD_ALL_SKIP_ADDING
|
||||||
return self.user_choice
|
return self.user_choice
|
||||||
|
|
||||||
|
class KnownNamesFilterDialog(QDialog):
|
||||||
|
"""A dialog to select names from Known.txt to add to the filter input."""
|
||||||
|
def __init__(self, known_names_list, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("Add Known Names to Filter")
|
||||||
|
self.setModal(True)
|
||||||
|
# Store the full list of known name objects. Each object is a dict.
|
||||||
|
# Sort them by the 'name' field for consistent display.
|
||||||
|
self.all_known_name_entries = sorted(known_names_list, key=lambda x: x['name'].lower())
|
||||||
|
self.selected_entries_to_return = []
|
||||||
|
|
||||||
|
main_layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
self.search_input = QLineEdit()
|
||||||
|
self.search_input.setPlaceholderText("Search names...")
|
||||||
|
self.search_input.textChanged.connect(self._filter_list_display)
|
||||||
|
main_layout.addWidget(self.search_input)
|
||||||
|
|
||||||
|
self.names_list_widget = QListWidget()
|
||||||
|
self._populate_list_widget() # Populate with all entries initially
|
||||||
|
main_layout.addWidget(self.names_list_widget)
|
||||||
|
|
||||||
|
# Buttons layout: Select All, Deselect All, Add, Cancel
|
||||||
|
buttons_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
self.select_all_button = QPushButton("Select All")
|
||||||
|
self.select_all_button.clicked.connect(self._select_all_items)
|
||||||
|
buttons_layout.addWidget(self.select_all_button) # Add to main buttons_layout
|
||||||
|
|
||||||
|
self.deselect_all_button = QPushButton("Deselect All")
|
||||||
|
self.deselect_all_button.clicked.connect(self._deselect_all_items)
|
||||||
|
buttons_layout.addWidget(self.deselect_all_button) # Add to main buttons_layout
|
||||||
|
buttons_layout.addStretch(1) # Stretch between Deselect All and Add Selected
|
||||||
|
|
||||||
|
self.add_button = QPushButton("Add Selected")
|
||||||
|
self.add_button.clicked.connect(self._accept_selection_action)
|
||||||
|
buttons_layout.addWidget(self.add_button)
|
||||||
|
|
||||||
|
self.cancel_button = QPushButton("Cancel")
|
||||||
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
|
buttons_layout.addWidget(self.cancel_button)
|
||||||
|
main_layout.addLayout(buttons_layout)
|
||||||
|
|
||||||
|
self.setMinimumWidth(350)
|
||||||
|
self.setMinimumHeight(400)
|
||||||
|
if parent and hasattr(parent, 'get_dark_theme'):
|
||||||
|
self.setStyleSheet(parent.get_dark_theme())
|
||||||
|
self.add_button.setDefault(True)
|
||||||
|
|
||||||
|
def _populate_list_widget(self, names_to_display=None):
|
||||||
|
self.names_list_widget.clear()
|
||||||
|
current_entries_source = names_to_display if names_to_display is not None else self.all_known_name_entries
|
||||||
|
for entry_obj in current_entries_source:
|
||||||
|
item = QListWidgetItem(entry_obj['name']) # Display the 'name' (folder name)
|
||||||
|
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||||
|
item.setCheckState(Qt.Unchecked)
|
||||||
|
item.setData(Qt.UserRole, entry_obj) # Store the full entry object
|
||||||
|
self.names_list_widget.addItem(item)
|
||||||
|
|
||||||
|
def _filter_list_display(self):
|
||||||
|
search_text = self.search_input.text().lower()
|
||||||
|
if not search_text:
|
||||||
|
self._populate_list_widget()
|
||||||
|
return
|
||||||
|
# Filter based on the 'name' field of each entry object
|
||||||
|
filtered_entries = [
|
||||||
|
entry_obj for entry_obj in self.all_known_name_entries if search_text in entry_obj['name'].lower()
|
||||||
|
]
|
||||||
|
self._populate_list_widget(filtered_entries)
|
||||||
|
|
||||||
|
def _accept_selection_action(self):
|
||||||
|
self.selected_entries_to_return = []
|
||||||
|
for i in range(self.names_list_widget.count()):
|
||||||
|
item = self.names_list_widget.item(i)
|
||||||
|
if item.checkState() == Qt.Checked:
|
||||||
|
self.selected_entries_to_return.append(item.data(Qt.UserRole)) # Get the stored entry object
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def _select_all_items(self):
|
||||||
|
"""Checks all items in the list widget."""
|
||||||
|
for i in range(self.names_list_widget.count()):
|
||||||
|
self.names_list_widget.item(i).setCheckState(Qt.Checked)
|
||||||
|
|
||||||
|
def _deselect_all_items(self):
|
||||||
|
"""Unchecks all items in the list widget."""
|
||||||
|
for i in range(self.names_list_widget.count()):
|
||||||
|
self.names_list_widget.item(i).setCheckState(Qt.Unchecked)
|
||||||
|
|
||||||
|
def get_selected_entries(self): # Renamed method
|
||||||
|
return self.selected_entries_to_return
|
||||||
|
|
||||||
class HelpGuideDialog(QDialog):
|
class HelpGuideDialog(QDialog):
|
||||||
"""A multi-page dialog for displaying the feature guide."""
|
"""A multi-page dialog for displaying the feature guide."""
|
||||||
def __init__(self, steps_data, parent=None):
|
def __init__(self, steps_data, parent=None):
|
||||||
@@ -521,7 +616,8 @@ class TourDialog(QDialog):
|
|||||||
" Enter character names, comma-separated (e.g., <i>Tifa, Aerith</i>). Group aliases for a combined folder name: <i>(alias1, alias2, alias3)</i> becomes folder 'alias1 alias2 alias3' (after cleaning). All names in the group are used as aliases for matching.<br>"
|
" Enter character names, comma-separated (e.g., <i>Tifa, Aerith</i>). Group aliases for a combined folder name: <i>(alias1, alias2, alias3)</i> becomes folder 'alias1 alias2 alias3' (after cleaning). All names in the group are used as aliases for matching.<br>"
|
||||||
" The <b>'Filter: [Type]'</b> button (next to this input) cycles how this filter applies:"
|
" The <b>'Filter: [Type]'</b> button (next to this input) cycles how this filter applies:"
|
||||||
" <ul><li><i>Filter: Files:</i> 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 (if 'Separate Folders' is on).</li><br>"
|
" <ul><li><i>Filter: Files:</i> 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 (if 'Separate Folders' is on).</li><br>"
|
||||||
" <li><i>Filter: Title:</i> Checks post titles. All files from a matching post are downloaded. Folder naming uses the character from the matching post title.</li><br>"
|
" <li><i>Filter: Title:</i> Checks post titles. All files from a matching post are downloaded. Folder naming uses the character from the matching post title.</li>"
|
||||||
|
" <li><b>⤵️ Add to Filter Button (Known Names):</b> Next to the 'Add' button for Known Names (see Step 5), this opens a popup. Select names from your <code>Known.txt</code> list via checkboxes (with a search bar) to quickly add them to the 'Filter by Character(s)' field. Grouped names like <code>(Boa, Hancock)</code> from Known.txt will be added as <code>(Boa, Hancock)~</code> to the filter.</li><br>" # Added new feature here
|
||||||
" <li><i>Filter: Both:</i> 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><br>"
|
" <li><i>Filter: Both:</i> 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><br>"
|
||||||
" <li><i>Filter: Comments (Beta):</i> 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><i>Filter: Comments (Beta):</i> 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>"
|
||||||
" This filter also influences folder naming if 'Separate Folders by Name/Title' is enabled.</li><br>"
|
" This filter also influences folder naming if 'Separate Folders by Name/Title' is enabled.</li><br>"
|
||||||
@@ -537,6 +633,7 @@ class TourDialog(QDialog):
|
|||||||
" <li><i>Images/GIFs:</i> Only common image formats and GIFs.</li><br>"
|
" <li><i>Images/GIFs:</i> Only common image formats and GIFs.</li><br>"
|
||||||
" <li><i>Videos:</i> Only common video formats.</li><br>"
|
" <li><i>Videos:</i> Only common video formats.</li><br>"
|
||||||
" <li><b><i>📦 Only Archives:</i></b> 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><br>"
|
" <li><b><i>📦 Only Archives:</i></b> 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><br>"
|
||||||
|
" <li><i>🎧 Only Audio:</i> Only common audio formats (MP3, WAV, FLAC, etc.).</li><br>"
|
||||||
" <li><i>🔗 Only Links:</i> Extracts and displays external links from post descriptions instead of downloading files. Download-related options and 'Show External Links' are disabled.</li>"
|
" <li><i>🔗 Only Links:</i> Extracts and displays external links from post descriptions instead of downloading files. Download-related options and 'Show External Links' are disabled.</li>"
|
||||||
" </ul></li>"
|
" </ul></li>"
|
||||||
"</ul>"
|
"</ul>"
|
||||||
@@ -584,12 +681,13 @@ class TourDialog(QDialog):
|
|||||||
" <li>The 'Page Range' input is disabled as all posts are fetched.</li><br>"
|
" <li>The 'Page Range' input is disabled as all posts are fetched.</li><br>"
|
||||||
" <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:"
|
" <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>"
|
" <ul>"
|
||||||
" <li><b><i>Name: Post Title (Default):</i></b> The first file in a post is named after the post's title. Subsequent files in the same post keep original names.</li><br>"
|
" <li><b><i>Name: Post Title (Default):</i></b> The first file in a post is named after the post's cleaned title (e.g., 'My Chapter 1.jpg'). Subsequent files within the *same post* will attempt to keep their original filenames (e.g., 'page_02.png', 'bonus_art.jpg'). If the post has only one file, it's named after the post title. This is generally recommended for most manga/comics.</li><br>"
|
||||||
" <li><b><i>Name: Original File:</i></b> All files attempt to keep their original filenames.</li><br>"
|
" <li><b><i>Name: Original File:</i></b> All files attempt to keep their original filenames. An optional prefix (e.g., 'MySeries_') can be entered in the input field that appears next to the style button. Example: 'MySeries_OriginalFile.jpg'.</li><br>"
|
||||||
" <li><b><i>Name: Date Based:</i></b> Files are named sequentially (001.ext, 002.ext, ...) based on post publication order. Multithreading for post processing is automatically disabled for this style.</li>"
|
" <li><b><i>Name: Title+G.Num (Post Title + Global Numbering):</i></b> All files across all posts in the current download session are named sequentially using the post's cleaned title as a prefix, followed by a global counter. For example: Post 'Chapter 1' (2 files) -> 'Chapter 1_001.jpg', 'Chapter 1_002.png'. The next post, 'Chapter 2' (1 file), would continue the numbering -> 'Chapter 2_003.jpg'. Multithreading for post processing is automatically disabled for this style to ensure correct global numbering.</li><br>"
|
||||||
|
" <li><b><i>Name: Date Based:</i></b> Files are named sequentially (001.ext, 002.ext, ...) based on post publication order. An optional prefix (e.g., 'MySeries_') can be entered in the input field that appears next to the style button. Example: 'MySeries_001.jpg'. Multithreading for post processing is automatically disabled for this style.</li>"
|
||||||
" </ul>"
|
" </ul>"
|
||||||
" </li><br>"
|
" </li><br>"
|
||||||
" <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>"
|
" <li>For best results with 'Name: Post Title', 'Name: Title+G.Num', or 'Name: Date Based' styles, use the 'Filter by Character(s)' field with the manga/series title for folder organization.</li>"
|
||||||
" </ul></li><br>"
|
" </ul></li><br>"
|
||||||
"<li><b>🎭 Known.txt for Smart Folder Organization:</b><br>"
|
"<li><b>🎭 Known.txt for Smart Folder Organization:</b><br>"
|
||||||
" <code>Known.txt</code> (in the app's directory) allows fine-grained control over automatic folder organization when 'Separate Folders by Name/Title' is active."
|
" <code>Known.txt</code> (in the app's directory) allows fine-grained control over automatic folder organization when 'Separate Folders by Name/Title' is active."
|
||||||
@@ -811,6 +909,12 @@ class DownloaderApp(QWidget):
|
|||||||
self.prompt_mutex = QMutex()
|
self.prompt_mutex = QMutex()
|
||||||
self._add_character_response = None
|
self._add_character_response = None
|
||||||
|
|
||||||
|
# Store original tooltips for dynamic updates. Label changed, tooltip content remains valid.
|
||||||
|
self._original_scan_content_tooltip = ("If checked, the downloader will scan the HTML content of posts for image URLs (from <img> tags or direct links).\n"
|
||||||
|
"now This includes resolving relative paths from <img> tags to full URLs.\n"
|
||||||
|
"Relative paths in <img> tags (e.g., /data/image.jpg) will be resolved to full URLs.\n"
|
||||||
|
"Useful for cases where images are in the post description but not in the API's file/attachment list.")
|
||||||
|
|
||||||
self.downloaded_files = set()
|
self.downloaded_files = set()
|
||||||
self.downloaded_files_lock = threading.Lock()
|
self.downloaded_files_lock = threading.Lock()
|
||||||
self.downloaded_file_hashes = set()
|
self.downloaded_file_hashes = set()
|
||||||
@@ -853,11 +957,12 @@ class DownloaderApp(QWidget):
|
|||||||
self.char_filter_scope = self.settings.value(CHAR_FILTER_SCOPE_KEY, CHAR_SCOPE_FILES, type=str) # Default to Files
|
self.char_filter_scope = self.settings.value(CHAR_FILTER_SCOPE_KEY, CHAR_SCOPE_FILES, type=str) # Default to Files
|
||||||
self.allow_multipart_download_setting = False
|
self.allow_multipart_download_setting = False
|
||||||
self.use_cookie_setting = False # Always default to False on launch
|
self.use_cookie_setting = False # Always default to False on launch
|
||||||
|
self.scan_content_images_setting = self.settings.value(SCAN_CONTENT_IMAGES_KEY, False, type=bool) # Load new setting
|
||||||
self.cookie_text_setting = "" # Always default to empty on launch
|
self.cookie_text_setting = "" # Always default to empty on launch
|
||||||
|
|
||||||
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.setWindowTitle("Kemono Downloader v4.1.1")
|
||||||
# self.load_known_names_from_util() # This call is premature and causes the error.
|
# self.load_known_names_from_util() # This call is premature and causes the error.
|
||||||
self.setStyleSheet(self.get_dark_theme())
|
self.setStyleSheet(self.get_dark_theme())
|
||||||
|
|
||||||
@@ -874,6 +979,7 @@ class DownloaderApp(QWidget):
|
|||||||
self.log_signal.emit(f"ℹ️ Multi-part download defaults to: {'Enabled' if self.allow_multipart_download_setting else 'Disabled'} on launch")
|
self.log_signal.emit(f"ℹ️ Multi-part download defaults to: {'Enabled' if self.allow_multipart_download_setting else 'Disabled'} on launch")
|
||||||
self.log_signal.emit(f"ℹ️ Cookie text defaults to: Empty on launch")
|
self.log_signal.emit(f"ℹ️ Cookie text defaults to: Empty on launch")
|
||||||
self.log_signal.emit(f"ℹ️ 'Use Cookie' setting defaults to: Disabled on launch")
|
self.log_signal.emit(f"ℹ️ 'Use Cookie' setting defaults to: Disabled on launch")
|
||||||
|
self.log_signal.emit(f"ℹ️ Scan post content for images defaults to: {'Enabled' if self.scan_content_images_setting else 'Disabled'}")
|
||||||
|
|
||||||
def _get_tooltip_for_character_input(self):
|
def _get_tooltip_for_character_input(self):
|
||||||
return (
|
return (
|
||||||
@@ -898,6 +1004,8 @@ class DownloaderApp(QWidget):
|
|||||||
self.cookie_browse_button.clicked.connect(self._browse_cookie_file)
|
self.cookie_browse_button.clicked.connect(self._browse_cookie_file)
|
||||||
if hasattr(self, 'cookie_text_input'): # Connect text changed for manual clear detection
|
if hasattr(self, 'cookie_text_input'): # Connect text changed for manual clear detection
|
||||||
self.cookie_text_input.textChanged.connect(self._handle_cookie_text_manual_change)
|
self.cookie_text_input.textChanged.connect(self._handle_cookie_text_manual_change)
|
||||||
|
if hasattr(self, 'download_thumbnails_checkbox'): # Connect the new handler
|
||||||
|
self.download_thumbnails_checkbox.toggled.connect(self._handle_thumbnail_mode_change)
|
||||||
self.gui_update_timer.timeout.connect(self._process_worker_queue)
|
self.gui_update_timer.timeout.connect(self._process_worker_queue)
|
||||||
self.gui_update_timer.start(100) # Check queue every 100ms
|
self.gui_update_timer.start(100) # Check queue every 100ms
|
||||||
self.log_signal.connect(self.handle_main_log)
|
self.log_signal.connect(self.handle_main_log)
|
||||||
@@ -940,6 +1048,9 @@ class DownloaderApp(QWidget):
|
|||||||
if hasattr(self, 'open_known_txt_button'): # Connect the new button
|
if hasattr(self, 'open_known_txt_button'): # Connect the new button
|
||||||
self.open_known_txt_button.clicked.connect(self._open_known_txt_file)
|
self.open_known_txt_button.clicked.connect(self._open_known_txt_file)
|
||||||
|
|
||||||
|
if hasattr(self, 'add_to_filter_button'): # Connect the new "Add to Filter" button
|
||||||
|
self.add_to_filter_button.clicked.connect(self._show_add_to_filter_dialog)
|
||||||
|
|
||||||
def _on_character_input_changed_live(self, text):
|
def _on_character_input_changed_live(self, text):
|
||||||
"""
|
"""
|
||||||
Called when the character input field text changes.
|
Called when the character input field text changes.
|
||||||
@@ -1119,6 +1230,7 @@ class DownloaderApp(QWidget):
|
|||||||
self.settings.setValue(CHAR_FILTER_SCOPE_KEY, self.char_filter_scope)
|
self.settings.setValue(CHAR_FILTER_SCOPE_KEY, self.char_filter_scope)
|
||||||
self.settings.setValue(ALLOW_MULTIPART_DOWNLOAD_KEY, self.allow_multipart_download_setting)
|
self.settings.setValue(ALLOW_MULTIPART_DOWNLOAD_KEY, self.allow_multipart_download_setting)
|
||||||
self.settings.setValue(COOKIE_TEXT_KEY, self.cookie_text_input.text() if hasattr(self, 'cookie_text_input') else "")
|
self.settings.setValue(COOKIE_TEXT_KEY, self.cookie_text_input.text() if hasattr(self, 'cookie_text_input') else "")
|
||||||
|
self.settings.setValue(SCAN_CONTENT_IMAGES_KEY, self.scan_content_images_checkbox.isChecked() if hasattr(self, 'scan_content_images_checkbox') else False)
|
||||||
self.settings.setValue(USE_COOKIE_KEY, self.use_cookie_checkbox.isChecked() if hasattr(self, 'use_cookie_checkbox') else False)
|
self.settings.setValue(USE_COOKIE_KEY, self.use_cookie_checkbox.isChecked() if hasattr(self, 'use_cookie_checkbox') else False)
|
||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
|
|
||||||
@@ -1331,6 +1443,7 @@ class DownloaderApp(QWidget):
|
|||||||
self.radio_videos = QRadioButton("Videos")
|
self.radio_videos = QRadioButton("Videos")
|
||||||
self.radio_videos.setToolTip("Download only common video formats (MP4, MKV, WEBM, MOV, etc.).")
|
self.radio_videos.setToolTip("Download only common video formats (MP4, MKV, WEBM, MOV, etc.).")
|
||||||
self.radio_only_archives = QRadioButton("📦 Only Archives")
|
self.radio_only_archives = QRadioButton("📦 Only Archives")
|
||||||
|
self.radio_only_audio = QRadioButton("🎧 Only Audio") # New Radio Button
|
||||||
self.radio_only_archives.setToolTip("Exclusively download .zip and .rar files. Other file-specific options are disabled.")
|
self.radio_only_archives.setToolTip("Exclusively download .zip and .rar files. Other file-specific options are disabled.")
|
||||||
self.radio_only_links = QRadioButton("🔗 Only Links")
|
self.radio_only_links = QRadioButton("🔗 Only Links")
|
||||||
self.radio_only_links.setToolTip("Extract and display external links from post descriptions instead of downloading files.\nDownload-related options will be disabled.")
|
self.radio_only_links.setToolTip("Extract and display external links from post descriptions instead of downloading files.\nDownload-related options will be disabled.")
|
||||||
@@ -1339,11 +1452,13 @@ class DownloaderApp(QWidget):
|
|||||||
self.radio_group.addButton(self.radio_images)
|
self.radio_group.addButton(self.radio_images)
|
||||||
self.radio_group.addButton(self.radio_videos)
|
self.radio_group.addButton(self.radio_videos)
|
||||||
self.radio_group.addButton(self.radio_only_archives)
|
self.radio_group.addButton(self.radio_only_archives)
|
||||||
|
self.radio_group.addButton(self.radio_only_audio) # Add to group
|
||||||
self.radio_group.addButton(self.radio_only_links)
|
self.radio_group.addButton(self.radio_only_links)
|
||||||
radio_button_layout.addWidget(self.radio_all)
|
radio_button_layout.addWidget(self.radio_all)
|
||||||
radio_button_layout.addWidget(self.radio_images)
|
radio_button_layout.addWidget(self.radio_images)
|
||||||
radio_button_layout.addWidget(self.radio_videos)
|
radio_button_layout.addWidget(self.radio_videos)
|
||||||
radio_button_layout.addWidget(self.radio_only_archives)
|
radio_button_layout.addWidget(self.radio_only_archives)
|
||||||
|
radio_button_layout.addWidget(self.radio_only_audio) # Add to layout
|
||||||
radio_button_layout.addWidget(self.radio_only_links)
|
radio_button_layout.addWidget(self.radio_only_links)
|
||||||
radio_button_layout.addStretch(1)
|
radio_button_layout.addStretch(1)
|
||||||
file_filter_layout.addLayout(radio_button_layout)
|
file_filter_layout.addLayout(radio_button_layout)
|
||||||
@@ -1364,12 +1479,23 @@ class DownloaderApp(QWidget):
|
|||||||
row1_layout.addWidget(self.skip_rar_checkbox)
|
row1_layout.addWidget(self.skip_rar_checkbox)
|
||||||
self.download_thumbnails_checkbox = QCheckBox("Download Thumbnails Only")
|
self.download_thumbnails_checkbox = QCheckBox("Download Thumbnails Only")
|
||||||
self.download_thumbnails_checkbox.setChecked(False)
|
self.download_thumbnails_checkbox.setChecked(False)
|
||||||
self.download_thumbnails_checkbox.setToolTip("Thumbnail download functionality is currently limited without the API.")
|
self.download_thumbnails_checkbox.setToolTip(
|
||||||
|
"Downloads small preview images from the API instead of full-sized files (if available).\n"
|
||||||
|
"If 'Scan Post Content for Image URLs' is also checked, this mode will *only* download images found by the content scan (ignoring API thumbnails)."
|
||||||
|
)
|
||||||
row1_layout.addWidget(self.download_thumbnails_checkbox)
|
row1_layout.addWidget(self.download_thumbnails_checkbox)
|
||||||
|
|
||||||
|
self.scan_content_images_checkbox = QCheckBox("Scan Content for Images") # Shortened Label
|
||||||
|
self.scan_content_images_checkbox.setToolTip(
|
||||||
|
self._original_scan_content_tooltip) # Use stored original tooltip
|
||||||
|
self.scan_content_images_checkbox.setChecked(self.scan_content_images_setting) # Set from loaded setting
|
||||||
|
row1_layout.addWidget(self.scan_content_images_checkbox) # Added to row1_layout
|
||||||
|
|
||||||
self.compress_images_checkbox = QCheckBox("Compress Large Images (to WebP)")
|
self.compress_images_checkbox = QCheckBox("Compress Large Images (to WebP)")
|
||||||
self.compress_images_checkbox.setChecked(False)
|
self.compress_images_checkbox.setChecked(False)
|
||||||
self.compress_images_checkbox.setToolTip("Compress images > 1.5MB to WebP format (requires Pillow).")
|
self.compress_images_checkbox.setToolTip("Compress images > 1.5MB to WebP format (requires Pillow).")
|
||||||
row1_layout.addWidget(self.compress_images_checkbox)
|
row1_layout.addWidget(self.compress_images_checkbox)
|
||||||
|
|
||||||
row1_layout.addStretch(1)
|
row1_layout.addStretch(1)
|
||||||
checkboxes_group_layout.addLayout(row1_layout)
|
checkboxes_group_layout.addLayout(row1_layout)
|
||||||
|
|
||||||
@@ -1457,6 +1583,7 @@ class DownloaderApp(QWidget):
|
|||||||
self.manga_mode_checkbox = QCheckBox("Manga/Comic Mode")
|
self.manga_mode_checkbox = QCheckBox("Manga/Comic Mode")
|
||||||
self.manga_mode_checkbox.setToolTip("Downloads posts from oldest to newest and renames files based on post title (for creator feeds only).")
|
self.manga_mode_checkbox.setToolTip("Downloads posts from oldest to newest and renames files based on post title (for creator feeds only).")
|
||||||
self.manga_mode_checkbox.setChecked(False)
|
self.manga_mode_checkbox.setChecked(False)
|
||||||
|
|
||||||
advanced_row2_layout.addWidget(self.manga_mode_checkbox) # Keep manga mode checkbox here
|
advanced_row2_layout.addWidget(self.manga_mode_checkbox) # Keep manga mode checkbox here
|
||||||
|
|
||||||
advanced_row2_layout.addStretch(1)
|
advanced_row2_layout.addStretch(1)
|
||||||
@@ -1516,12 +1643,17 @@ class DownloaderApp(QWidget):
|
|||||||
self.new_char_input.setPlaceholderText("Add new show/character name")
|
self.new_char_input.setPlaceholderText("Add new show/character name")
|
||||||
self.add_char_button = QPushButton("➕ Add")
|
self.add_char_button = QPushButton("➕ Add")
|
||||||
self.add_char_button.setToolTip("Add the name from the input field to the 'Known Shows/Characters' list.")
|
self.add_char_button.setToolTip("Add the name from the input field to the 'Known Shows/Characters' list.")
|
||||||
|
|
||||||
|
self.add_to_filter_button = QPushButton("⤵️ Add to Filter") # New Button
|
||||||
|
self.add_to_filter_button.setToolTip("Select names from 'Known Shows/Characters' list to add to the 'Filter by Character(s)' field above.")
|
||||||
|
|
||||||
self.delete_char_button = QPushButton("🗑️ Delete Selected")
|
self.delete_char_button = QPushButton("🗑️ Delete Selected")
|
||||||
self.delete_char_button.setToolTip("Delete the selected name(s) from the 'Known Shows/Characters' list.")
|
self.delete_char_button.setToolTip("Delete the selected name(s) from the 'Known Shows/Characters' list.")
|
||||||
# Connect add_char_button to a new handler that calls the refactored add_new_character
|
# Connect add_char_button to a new handler that calls the refactored add_new_character
|
||||||
self.add_char_button.clicked.connect(self._handle_ui_add_new_character)
|
self.add_char_button.clicked.connect(self._handle_ui_add_new_character)
|
||||||
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, 0)
|
char_manage_layout.addWidget(self.add_char_button, 0)
|
||||||
|
|
||||||
@@ -1533,6 +1665,7 @@ class DownloaderApp(QWidget):
|
|||||||
self.known_names_help_button.clicked.connect(self._show_feature_guide)
|
self.known_names_help_button.clicked.connect(self._show_feature_guide)
|
||||||
|
|
||||||
|
|
||||||
|
char_manage_layout.addWidget(self.add_to_filter_button, 0) # Add new button to layout
|
||||||
char_manage_layout.addWidget(self.delete_char_button, 0)
|
char_manage_layout.addWidget(self.delete_char_button, 0)
|
||||||
char_manage_layout.addWidget(self.known_names_help_button, 0) # Moved to the end (rightmost)
|
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)
|
||||||
@@ -1563,6 +1696,14 @@ class DownloaderApp(QWidget):
|
|||||||
self._update_manga_filename_style_button_text()
|
self._update_manga_filename_style_button_text()
|
||||||
log_title_layout.addWidget(self.manga_rename_toggle_button)
|
log_title_layout.addWidget(self.manga_rename_toggle_button)
|
||||||
|
|
||||||
|
# NEW: Manga Date Prefix Input
|
||||||
|
self.manga_date_prefix_input = QLineEdit()
|
||||||
|
self.manga_date_prefix_input.setPlaceholderText("Prefix for Manga Filenames") # Generalized
|
||||||
|
self.manga_date_prefix_input.setToolTip("Optional prefix for 'Date Based' or 'Original File' manga filenames (e.g., 'Series Name').\nIf empty, files will be named based on the style without a prefix.") # Generalized
|
||||||
|
self.manga_date_prefix_input.setVisible(False) # Initially hidden
|
||||||
|
self.manga_date_prefix_input.setFixedWidth(160) # Adjust as needed
|
||||||
|
log_title_layout.addWidget(self.manga_date_prefix_input) # Add to layout
|
||||||
|
|
||||||
self.multipart_toggle_button = QPushButton()
|
self.multipart_toggle_button = QPushButton()
|
||||||
self.multipart_toggle_button.setToolTip("Toggle between Multi-part and Single-stream downloads for large files.")
|
self.multipart_toggle_button.setToolTip("Toggle between Multi-part and Single-stream downloads for large files.")
|
||||||
self.multipart_toggle_button.setFixedWidth(130) # Adjust width as needed
|
self.multipart_toggle_button.setFixedWidth(130) # Adjust width as needed
|
||||||
@@ -1669,6 +1810,8 @@ class DownloaderApp(QWidget):
|
|||||||
self._update_skip_scope_button_text()
|
self._update_skip_scope_button_text()
|
||||||
self._update_char_filter_scope_button_text()
|
self._update_char_filter_scope_button_text()
|
||||||
self._update_multithreading_for_date_mode() # Ensure correct initial state
|
self._update_multithreading_for_date_mode() # Ensure correct initial state
|
||||||
|
if hasattr(self, 'download_thumbnails_checkbox'): # Set initial state for scan_content checkbox based on thumbnail checkbox
|
||||||
|
self._handle_thumbnail_mode_change(self.download_thumbnails_checkbox.isChecked())
|
||||||
|
|
||||||
def _browse_cookie_file(self):
|
def _browse_cookie_file(self):
|
||||||
"""Opens a file dialog to select a cookie file."""
|
"""Opens a file dialog to select a cookie file."""
|
||||||
@@ -1990,11 +2133,13 @@ class DownloaderApp(QWidget):
|
|||||||
|
|
||||||
filter_mode_text = button.text()
|
filter_mode_text = button.text()
|
||||||
is_only_links = (filter_mode_text == "🔗 Only Links")
|
is_only_links = (filter_mode_text == "🔗 Only Links")
|
||||||
|
is_only_audio = (filter_mode_text == "🎧 Only Audio")
|
||||||
is_only_archives = (filter_mode_text == "📦 Only Archives")
|
is_only_archives = (filter_mode_text == "📦 Only Archives")
|
||||||
|
|
||||||
if self.skip_scope_toggle_button:
|
if self.skip_scope_toggle_button:
|
||||||
self.skip_scope_toggle_button.setVisible(not (is_only_links or is_only_archives))
|
self.skip_scope_toggle_button.setVisible(not (is_only_links or is_only_archives or is_only_audio))
|
||||||
if hasattr(self, 'multipart_toggle_button') and self.multipart_toggle_button:
|
if hasattr(self, 'multipart_toggle_button') and self.multipart_toggle_button:
|
||||||
self.multipart_toggle_button.setVisible(not (is_only_links or is_only_archives))
|
self.multipart_toggle_button.setVisible(not (is_only_links or is_only_archives or is_only_audio))
|
||||||
|
|
||||||
if self.link_search_input: self.link_search_input.setVisible(is_only_links)
|
if self.link_search_input: self.link_search_input.setVisible(is_only_links)
|
||||||
if self.link_search_button: self.link_search_button.setVisible(is_only_links)
|
if self.link_search_button: self.link_search_button.setVisible(is_only_links)
|
||||||
@@ -2009,7 +2154,7 @@ class DownloaderApp(QWidget):
|
|||||||
self.download_btn.setText("⬇️ Start Download")
|
self.download_btn.setText("⬇️ Start Download")
|
||||||
if not is_only_links and self.link_search_input: self.link_search_input.clear()
|
if not is_only_links and self.link_search_input: self.link_search_input.clear()
|
||||||
|
|
||||||
file_download_mode_active = not is_only_links
|
file_download_mode_active = not is_only_links # Audio mode is a file download mode
|
||||||
|
|
||||||
if self.dir_input: self.dir_input.setEnabled(file_download_mode_active)
|
if self.dir_input: self.dir_input.setEnabled(file_download_mode_active)
|
||||||
if self.dir_button: self.dir_button.setEnabled(file_download_mode_active)
|
if self.dir_button: self.dir_button.setEnabled(file_download_mode_active)
|
||||||
@@ -2019,23 +2164,23 @@ class DownloaderApp(QWidget):
|
|||||||
if hasattr(self, 'remove_from_filename_input'): self.remove_from_filename_input.setEnabled(file_download_mode_active)
|
if hasattr(self, 'remove_from_filename_input'): self.remove_from_filename_input.setEnabled(file_download_mode_active)
|
||||||
|
|
||||||
if self.skip_zip_checkbox:
|
if self.skip_zip_checkbox:
|
||||||
can_skip_zip = not is_only_links and not is_only_archives
|
can_skip_zip = file_download_mode_active and not is_only_archives # Audio mode allows skipping zip
|
||||||
self.skip_zip_checkbox.setEnabled(can_skip_zip)
|
self.skip_zip_checkbox.setEnabled(can_skip_zip)
|
||||||
if is_only_archives:
|
if is_only_archives:
|
||||||
self.skip_zip_checkbox.setChecked(False)
|
self.skip_zip_checkbox.setChecked(False)
|
||||||
|
|
||||||
if self.skip_rar_checkbox:
|
if self.skip_rar_checkbox:
|
||||||
can_skip_rar = not is_only_links and not is_only_archives
|
can_skip_rar = file_download_mode_active and not is_only_archives # Audio mode allows skipping rar
|
||||||
self.skip_rar_checkbox.setEnabled(can_skip_rar)
|
self.skip_rar_checkbox.setEnabled(can_skip_rar)
|
||||||
if is_only_archives:
|
if is_only_archives:
|
||||||
self.skip_rar_checkbox.setChecked(False)
|
self.skip_rar_checkbox.setChecked(False)
|
||||||
|
|
||||||
other_file_proc_enabled = not is_only_links and not is_only_archives
|
other_file_proc_enabled = file_download_mode_active and not is_only_archives # Thumbnails/compression relevant if not archives
|
||||||
if self.download_thumbnails_checkbox: self.download_thumbnails_checkbox.setEnabled(other_file_proc_enabled)
|
if self.download_thumbnails_checkbox: self.download_thumbnails_checkbox.setEnabled(other_file_proc_enabled)
|
||||||
if self.compress_images_checkbox: self.compress_images_checkbox.setEnabled(other_file_proc_enabled)
|
if self.compress_images_checkbox: self.compress_images_checkbox.setEnabled(other_file_proc_enabled)
|
||||||
|
|
||||||
if self.external_links_checkbox:
|
if self.external_links_checkbox:
|
||||||
can_show_external_log_option = not is_only_links and not is_only_archives
|
can_show_external_log_option = file_download_mode_active and not is_only_archives # External links relevant if not archives
|
||||||
self.external_links_checkbox.setEnabled(can_show_external_log_option)
|
self.external_links_checkbox.setEnabled(can_show_external_log_option)
|
||||||
if not can_show_external_log_option:
|
if not can_show_external_log_option:
|
||||||
self.external_links_checkbox.setChecked(False)
|
self.external_links_checkbox.setChecked(False)
|
||||||
@@ -2056,6 +2201,12 @@ class DownloaderApp(QWidget):
|
|||||||
if self.log_splitter: self.log_splitter.setSizes([self.height(), 0])
|
if self.log_splitter: self.log_splitter.setSizes([self.height(), 0])
|
||||||
if self.main_log_output: self.main_log_output.clear()
|
if self.main_log_output: self.main_log_output.clear()
|
||||||
self.log_signal.emit("="*20 + " Mode changed to: Only Archives " + "="*20)
|
self.log_signal.emit("="*20 + " Mode changed to: Only Archives " + "="*20)
|
||||||
|
elif is_only_audio:
|
||||||
|
self.progress_log_label.setText("📜 Progress Log (Audio Only):")
|
||||||
|
if self.external_log_output: self.external_log_output.hide() # Typically no external log for specific content types unless explicitly enabled
|
||||||
|
if self.log_splitter: self.log_splitter.setSizes([self.height(), 0])
|
||||||
|
if self.main_log_output: self.main_log_output.clear()
|
||||||
|
self.log_signal.emit("="*20 + " Mode changed to: Only Archives " + "="*20)
|
||||||
else:
|
else:
|
||||||
self.progress_log_label.setText("📜 Progress Log:")
|
self.progress_log_label.setText("📜 Progress Log:")
|
||||||
self.update_external_links_setting(self.external_links_checkbox.isChecked() if self.external_links_checkbox else False)
|
self.update_external_links_setting(self.external_links_checkbox.isChecked() if self.external_links_checkbox else False)
|
||||||
@@ -2066,7 +2217,7 @@ class DownloaderApp(QWidget):
|
|||||||
|
|
||||||
# Determine if character filter section should be active (visible and enabled)
|
# 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')
|
# 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
|
character_filter_should_be_active = file_download_mode_active and not is_only_archives
|
||||||
|
|
||||||
if self.character_filter_widget:
|
if self.character_filter_widget:
|
||||||
self.character_filter_widget.setVisible(character_filter_should_be_active)
|
self.character_filter_widget.setVisible(character_filter_should_be_active)
|
||||||
@@ -2160,6 +2311,8 @@ class DownloaderApp(QWidget):
|
|||||||
return 'video'
|
return 'video'
|
||||||
elif self.radio_only_archives and self.radio_only_archives.isChecked():
|
elif self.radio_only_archives and self.radio_only_archives.isChecked():
|
||||||
return 'archive'
|
return 'archive'
|
||||||
|
elif hasattr(self, 'radio_only_audio') and self.radio_only_audio.isChecked(): # New audio mode
|
||||||
|
return 'audio'
|
||||||
elif self.radio_all.isChecked():
|
elif self.radio_all.isChecked():
|
||||||
return 'all'
|
return 'all'
|
||||||
return 'all'
|
return 'all'
|
||||||
@@ -2463,7 +2616,8 @@ class DownloaderApp(QWidget):
|
|||||||
|
|
||||||
not_only_links_or_archives_mode = not (
|
not_only_links_or_archives_mode = not (
|
||||||
(self.radio_only_links and self.radio_only_links.isChecked()) or
|
(self.radio_only_links and self.radio_only_links.isChecked()) or
|
||||||
(self.radio_only_archives and self.radio_only_archives.isChecked())
|
(self.radio_only_archives and self.radio_only_archives.isChecked()) or
|
||||||
|
(hasattr(self, 'radio_only_audio') and self.radio_only_audio.isChecked()) # Audio mode also hides custom folder
|
||||||
)
|
)
|
||||||
|
|
||||||
should_show_custom_folder = is_single_post_url and subfolders_enabled and not_only_links_or_archives_mode
|
should_show_custom_folder = is_single_post_url and subfolders_enabled and not_only_links_or_archives_mode
|
||||||
@@ -2478,7 +2632,10 @@ class DownloaderApp(QWidget):
|
|||||||
def update_ui_for_subfolders(self, separate_folders_by_name_title_checked: bool):
|
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()
|
||||||
|
is_only_audio = hasattr(self, 'radio_only_audio') and self.radio_only_audio.isChecked()
|
||||||
|
|
||||||
|
# "Subfolder per Post" can be enabled if it's not "Only Links" and not "Only Archives".
|
||||||
|
# This means it CAN be enabled for "All", "Images/GIFs", "Videos", and "Only Audio" modes.
|
||||||
can_enable_subfolder_per_post_checkbox = not is_only_links and not is_only_archives
|
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:
|
||||||
@@ -2552,10 +2709,10 @@ class DownloaderApp(QWidget):
|
|||||||
elif self.manga_filename_style == STYLE_ORIGINAL_NAME:
|
elif self.manga_filename_style == STYLE_ORIGINAL_NAME:
|
||||||
self.manga_rename_toggle_button.setText("Name: Original File")
|
self.manga_rename_toggle_button.setText("Name: Original File")
|
||||||
self.manga_rename_toggle_button.setToolTip(
|
self.manga_rename_toggle_button.setToolTip(
|
||||||
"Manga Filename Style: Original File Name\n\n"
|
"Manga Filename Style: Original File Name\n\n" # Updated tooltip
|
||||||
"When Manga/Comic Mode is active for a creator feed:\n"
|
"When Manga/Comic Mode is active for a creator feed:\n"
|
||||||
"- *All* files in a post will attempt to keep their original filenames as provided by the site (e.g., \"001.jpg\", \"page_02.png\").\n"
|
"- *All* files in a post will attempt to keep their original filenames as provided by the site (e.g., \"001.jpg\", \"page_02.png\").\n"
|
||||||
"- This can be useful if original names are already well-structured and sequential.\n"
|
"- An optional prefix can be entered in the field next to this button (e.g., 'MySeries_001.jpg').\n"
|
||||||
"- If original names are inconsistent, using \"Post Title\" style is often better.\n"
|
"- If original names are inconsistent, using \"Post Title\" style is often better.\n"
|
||||||
"- Example: Post \"Chapter 1: The Beginning\" with files \"001.jpg\", \"002.jpg\".\n"
|
"- Example: Post \"Chapter 1: The Beginning\" with files \"001.jpg\", \"002.jpg\".\n"
|
||||||
" Downloads as: \"001.jpg\", \"002.jpg\".\n\n"
|
" Downloads as: \"001.jpg\", \"002.jpg\".\n\n"
|
||||||
@@ -2578,7 +2735,8 @@ class DownloaderApp(QWidget):
|
|||||||
self.manga_rename_toggle_button.setToolTip(
|
self.manga_rename_toggle_button.setToolTip(
|
||||||
"Manga Filename Style: Date Based\n\n"
|
"Manga Filename Style: Date Based\n\n"
|
||||||
"When Manga/Comic Mode is active for a creator feed:\n"
|
"When Manga/Comic Mode is active for a creator feed:\n"
|
||||||
"- Files will be named sequentially (001.ext, 002.ext, ...) based on post publication order (oldest to newest).\n"
|
"- Files will be named sequentially (001.ext, 002.ext, ...) based on post publication order.\n"
|
||||||
|
"- An optional prefix can be entered in the field next to this button (e.g., 'MySeries_001.jpg').\n"
|
||||||
"- To ensure correct numbering, multithreading for post processing is automatically disabled when this style is active.\n\n"
|
"- To ensure correct numbering, multithreading for post processing is automatically disabled when this style is active.\n\n"
|
||||||
"Click to change to: Post Title"
|
"Click to change to: Post Title"
|
||||||
)
|
)
|
||||||
@@ -2612,13 +2770,14 @@ class DownloaderApp(QWidget):
|
|||||||
self.settings.setValue(MANGA_FILENAME_STYLE_KEY, self.manga_filename_style)
|
self.settings.setValue(MANGA_FILENAME_STYLE_KEY, self.manga_filename_style)
|
||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
self._update_manga_filename_style_button_text()
|
self._update_manga_filename_style_button_text()
|
||||||
self._update_multithreading_for_date_mode() # Update multithreading state based on new style
|
self.update_ui_for_manga_mode(self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False) # Update UI based on new style
|
||||||
self.log_signal.emit(f"ℹ️ Manga filename style changed to: '{self.manga_filename_style}'")
|
self.log_signal.emit(f"ℹ️ Manga filename style changed to: '{self.manga_filename_style}'")
|
||||||
|
|
||||||
|
|
||||||
def update_ui_for_manga_mode(self, checked):
|
def update_ui_for_manga_mode(self, checked):
|
||||||
is_only_links_mode = self.radio_only_links and self.radio_only_links.isChecked()
|
is_only_links_mode = self.radio_only_links and self.radio_only_links.isChecked()
|
||||||
is_only_archives_mode = self.radio_only_archives and self.radio_only_archives.isChecked()
|
is_only_archives_mode = self.radio_only_archives and self.radio_only_archives.isChecked()
|
||||||
|
is_only_audio_mode = hasattr(self, 'radio_only_audio') and self.radio_only_audio.isChecked()
|
||||||
|
|
||||||
url_text = self.link_input.text().strip() if self.link_input else ""
|
url_text = self.link_input.text().strip() if self.link_input else ""
|
||||||
_, _, post_id = extract_post_info(url_text)
|
_, _, post_id = extract_post_info(url_text)
|
||||||
@@ -2633,14 +2792,17 @@ class DownloaderApp(QWidget):
|
|||||||
manga_mode_effectively_on = is_creator_feed and checked
|
manga_mode_effectively_on = is_creator_feed and checked
|
||||||
|
|
||||||
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 or is_only_audio_mode))
|
||||||
|
|
||||||
# Always update page range enabled state, as it depends on URL type, not directly manga mode.
|
# Always update page range enabled state, as it depends on URL type, not directly manga mode.
|
||||||
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())
|
current_filename_style = self.manga_filename_style
|
||||||
# Character filter widgets should be enabled if it's a file download mode
|
|
||||||
enable_char_filter_widgets = file_download_mode_active and not (self.radio_only_archives and self.radio_only_archives.isChecked())
|
# Character filter widgets should be enabled if it's a file download mode where character
|
||||||
|
# filtering makes sense (i.e., not 'Only Links' and not 'Only Archives').
|
||||||
|
# 'Only Audio' mode is a file download mode where character filters are applicable.
|
||||||
|
enable_char_filter_widgets = not is_only_links_mode and not is_only_archives_mode
|
||||||
|
|
||||||
if self.character_input:
|
if self.character_input:
|
||||||
self.character_input.setEnabled(enable_char_filter_widgets)
|
self.character_input.setEnabled(enable_char_filter_widgets)
|
||||||
@@ -2649,6 +2811,23 @@ class DownloaderApp(QWidget):
|
|||||||
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
|
if self.character_filter_widget: # Also ensure the main widget visibility is correct
|
||||||
self.character_filter_widget.setVisible(enable_char_filter_widgets)
|
self.character_filter_widget.setVisible(enable_char_filter_widgets)
|
||||||
|
|
||||||
|
# Visibility for manga date prefix input
|
||||||
|
show_date_prefix_input = (
|
||||||
|
manga_mode_effectively_on and
|
||||||
|
(current_filename_style == STYLE_DATE_BASED or current_filename_style == STYLE_ORIGINAL_NAME) and # MODIFIED
|
||||||
|
not (is_only_links_mode or is_only_archives_mode or is_only_audio_mode)
|
||||||
|
)
|
||||||
|
if hasattr(self, 'manga_date_prefix_input'):
|
||||||
|
self.manga_date_prefix_input.setVisible(show_date_prefix_input)
|
||||||
|
if not show_date_prefix_input: # Clear if not visible
|
||||||
|
self.manga_date_prefix_input.clear()
|
||||||
|
|
||||||
|
# Visibility for multipart toggle button
|
||||||
|
if hasattr(self, 'multipart_toggle_button'):
|
||||||
|
show_multipart_button = not (show_date_prefix_input or is_only_links_mode or is_only_archives_mode or is_only_audio_mode)
|
||||||
|
self.multipart_toggle_button.setVisible(show_multipart_button)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -2787,6 +2966,7 @@ class DownloaderApp(QWidget):
|
|||||||
raw_remove_filename_words = self.remove_from_filename_input.text().strip() if hasattr(self, 'remove_from_filename_input') else ""
|
raw_remove_filename_words = self.remove_from_filename_input.text().strip() if hasattr(self, 'remove_from_filename_input') else ""
|
||||||
allow_multipart = self.allow_multipart_download_setting # Use the internal setting
|
allow_multipart = self.allow_multipart_download_setting # Use the internal setting
|
||||||
remove_from_filename_words_list = [word.strip() for word in raw_remove_filename_words.split(',') if word.strip()]
|
remove_from_filename_words_list = [word.strip() for word in raw_remove_filename_words.split(',') if word.strip()]
|
||||||
|
scan_content_for_images = self.scan_content_images_checkbox.isChecked() if hasattr(self, 'scan_content_images_checkbox') else False
|
||||||
use_cookie_from_checkbox = self.use_cookie_checkbox.isChecked() if hasattr(self, 'use_cookie_checkbox') else False
|
use_cookie_from_checkbox = self.use_cookie_checkbox.isChecked() if hasattr(self, 'use_cookie_checkbox') else False
|
||||||
app_base_dir_for_cookies = os.path.dirname(self.config_file) # Directory of Known.txt
|
app_base_dir_for_cookies = os.path.dirname(self.config_file) # Directory of Known.txt
|
||||||
cookie_text_from_input = self.cookie_text_input.text().strip() if hasattr(self, 'cookie_text_input') and use_cookie_from_checkbox else ""
|
cookie_text_from_input = self.cookie_text_input.text().strip() if hasattr(self, 'cookie_text_input') and use_cookie_from_checkbox else ""
|
||||||
@@ -2797,7 +2977,8 @@ class DownloaderApp(QWidget):
|
|||||||
|
|
||||||
extract_links_only = (self.radio_only_links and self.radio_only_links.isChecked())
|
extract_links_only = (self.radio_only_links and self.radio_only_links.isChecked())
|
||||||
backend_filter_mode = self.get_filter_mode()
|
backend_filter_mode = self.get_filter_mode()
|
||||||
user_selected_filter_text = self.radio_group.checkedButton().text() if self.radio_group.checkedButton() else "All"
|
checked_radio_button = self.radio_group.checkedButton()
|
||||||
|
user_selected_filter_text = checked_radio_button.text() if checked_radio_button else "All"
|
||||||
if selected_cookie_file_path_for_backend:
|
if selected_cookie_file_path_for_backend:
|
||||||
cookie_text_from_input = ""
|
cookie_text_from_input = ""
|
||||||
|
|
||||||
@@ -2807,6 +2988,9 @@ class DownloaderApp(QWidget):
|
|||||||
else:
|
else:
|
||||||
effective_skip_zip = self.skip_zip_checkbox.isChecked()
|
effective_skip_zip = self.skip_zip_checkbox.isChecked()
|
||||||
effective_skip_rar = self.skip_rar_checkbox.isChecked()
|
effective_skip_rar = self.skip_rar_checkbox.isChecked()
|
||||||
|
if backend_filter_mode == 'audio': # If audio mode, don't skip archives by default unless user explicitly checks
|
||||||
|
effective_skip_zip = self.skip_zip_checkbox.isChecked() # Keep user's choice
|
||||||
|
effective_skip_rar = self.skip_rar_checkbox.isChecked() # Keep user's choice
|
||||||
|
|
||||||
if not api_url: QMessageBox.critical(self, "Input Error", "URL is required."); return
|
if not api_url: QMessageBox.critical(self, "Input Error", "URL is required."); return
|
||||||
if not extract_links_only and not output_dir:
|
if not extract_links_only and not output_dir:
|
||||||
@@ -2819,7 +3003,7 @@ class DownloaderApp(QWidget):
|
|||||||
if not extract_links_only and not os.path.isdir(output_dir):
|
if not extract_links_only and not os.path.isdir(output_dir):
|
||||||
reply = QMessageBox.question(self, "Create Directory?",
|
reply = QMessageBox.question(self, "Create Directory?",
|
||||||
f"The directory '{output_dir}' does not exist.\nCreate it now?",
|
f"The directory '{output_dir}' does not exist.\nCreate it now?",
|
||||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) # type: ignore
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
try: os.makedirs(output_dir, exist_ok=True); self.log_signal.emit(f"ℹ️ Created directory: {output_dir}")
|
try: os.makedirs(output_dir, exist_ok=True); self.log_signal.emit(f"ℹ️ Created directory: {output_dir}")
|
||||||
except Exception as e: QMessageBox.critical(self, "Directory Error", f"Could not create directory: {e}"); return
|
except Exception as e: QMessageBox.critical(self, "Directory Error", f"Could not create directory: {e}"); return
|
||||||
@@ -2829,8 +3013,22 @@ class DownloaderApp(QWidget):
|
|||||||
QMessageBox.warning(self, "Missing Dependency", "Pillow library (for image compression) not found. Compression will be disabled.")
|
QMessageBox.warning(self, "Missing Dependency", "Pillow library (for image compression) not found. Compression will be disabled.")
|
||||||
compress_images = False; self.compress_images_checkbox.setChecked(False)
|
compress_images = False; self.compress_images_checkbox.setChecked(False)
|
||||||
|
|
||||||
|
# Initialize log_messages here, before it's potentially used by manga_date_prefix_text logging
|
||||||
|
log_messages = ["="*40, f"🚀 Starting {'Link Extraction' if extract_links_only else ('Archive Download' if backend_filter_mode == 'archive' else 'Download')} @ {time.strftime('%Y-%m-%d %H:%M:%S')}", f" URL: {api_url}"]
|
||||||
|
|
||||||
|
current_mode_log_text = "Download"
|
||||||
|
if extract_links_only: current_mode_log_text = "Link Extraction"
|
||||||
|
elif backend_filter_mode == 'archive': current_mode_log_text = "Archive Download"
|
||||||
|
elif backend_filter_mode == 'audio': current_mode_log_text = "Audio Download"
|
||||||
manga_mode = manga_mode_is_checked and not post_id_from_url
|
manga_mode = manga_mode_is_checked and not post_id_from_url
|
||||||
|
|
||||||
|
manga_date_prefix_text = ""
|
||||||
|
if manga_mode and \
|
||||||
|
(self.manga_filename_style == STYLE_DATE_BASED or self.manga_filename_style == STYLE_ORIGINAL_NAME) and \
|
||||||
|
hasattr(self, 'manga_date_prefix_input'):
|
||||||
|
manga_date_prefix_text = self.manga_date_prefix_input.text().strip()
|
||||||
|
if manga_date_prefix_text: # Log only if prefix is provided (log_messages is now initialized)
|
||||||
|
log_messages.append(f" ↳ Manga Date Prefix: '{manga_date_prefix_text}'")
|
||||||
|
|
||||||
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
|
||||||
@@ -3031,7 +3229,7 @@ class DownloaderApp(QWidget):
|
|||||||
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
|
||||||
effective_num_file_threads_per_worker = 1 # Files within each post worker are sequential
|
effective_num_file_threads_per_worker = 1 # Files within each post worker are sequential
|
||||||
|
|
||||||
log_messages = ["="*40, f"🚀 Starting {'Link Extraction' if extract_links_only else ('Archive Download' if backend_filter_mode == 'archive' else 'Download')} @ {time.strftime('%Y-%m-%d %H:%M:%S')}", f" URL: {api_url}"]
|
# log_messages initialization was moved earlier
|
||||||
if not extract_links_only: log_messages.append(f" Save Location: {output_dir}")
|
if not extract_links_only: log_messages.append(f" Save Location: {output_dir}")
|
||||||
|
|
||||||
if post_id_from_url:
|
if post_id_from_url:
|
||||||
@@ -3072,6 +3270,7 @@ class DownloaderApp(QWidget):
|
|||||||
f" Compress Images: {'Enabled' if compress_images else 'Disabled'}",
|
f" Compress Images: {'Enabled' if compress_images else 'Disabled'}",
|
||||||
f" Thumbnails Only: {'Enabled' if download_thumbnails else 'Disabled'}" # Removed duplicate file handling log
|
f" Thumbnails Only: {'Enabled' if download_thumbnails else 'Disabled'}" # Removed duplicate file handling log
|
||||||
])
|
])
|
||||||
|
log_messages.append(f" Scan Post Content for Images: {'Enabled' if scan_content_for_images else 'Disabled'}")
|
||||||
else:
|
else:
|
||||||
log_messages.append(f" Mode: Extracting Links Only")
|
log_messages.append(f" Mode: Extracting Links Only")
|
||||||
|
|
||||||
@@ -3138,8 +3337,10 @@ class DownloaderApp(QWidget):
|
|||||||
'manga_mode_active': manga_mode,
|
'manga_mode_active': manga_mode,
|
||||||
'unwanted_keywords': unwanted_keywords_for_folders,
|
'unwanted_keywords': unwanted_keywords_for_folders,
|
||||||
'cancellation_event': self.cancellation_event,
|
'cancellation_event': self.cancellation_event,
|
||||||
|
'manga_date_prefix': manga_date_prefix_text, # NEW ARGUMENT
|
||||||
'dynamic_character_filter_holder': self.dynamic_character_filter_holder, # Pass the holder
|
'dynamic_character_filter_holder': self.dynamic_character_filter_holder, # Pass the holder
|
||||||
'pause_event': self.pause_event, # Explicitly add pause_event here
|
'pause_event': self.pause_event, # Explicitly add pause_event here
|
||||||
|
'scan_content_for_images': scan_content_for_images, # Pass new flag
|
||||||
'manga_filename_style': self.manga_filename_style,
|
'manga_filename_style': self.manga_filename_style,
|
||||||
'num_file_threads_for_worker': effective_num_file_threads_per_worker,
|
'num_file_threads_for_worker': effective_num_file_threads_per_worker,
|
||||||
'manga_date_file_counter_ref': manga_date_file_counter_ref_for_thread,
|
'manga_date_file_counter_ref': manga_date_file_counter_ref_for_thread,
|
||||||
@@ -3153,7 +3354,7 @@ class DownloaderApp(QWidget):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if should_use_multithreading_for_posts:
|
if should_use_multithreading_for_posts:
|
||||||
self.log_signal.emit(f" Initializing multi-threaded {'link extraction' if extract_links_only else 'download'} with {effective_num_post_workers} post workers...")
|
self.log_signal.emit(f" Initializing multi-threaded {current_mode_log_text.lower()} with {effective_num_post_workers} post workers...")
|
||||||
args_template['emitter'] = self.worker_to_gui_queue # For multi-threaded, use the queue
|
args_template['emitter'] = self.worker_to_gui_queue # For multi-threaded, use the queue
|
||||||
self.start_multi_threaded_download(num_post_workers=effective_num_post_workers, **args_template)
|
self.start_multi_threaded_download(num_post_workers=effective_num_post_workers, **args_template)
|
||||||
else:
|
else:
|
||||||
@@ -3169,8 +3370,8 @@ class DownloaderApp(QWidget):
|
|||||||
'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',
|
'manga_date_file_counter_ref',
|
||||||
'manga_global_file_counter_ref', # Pass new counter for single thread mode
|
'manga_global_file_counter_ref', 'manga_date_prefix', # Pass new counter and prefix for single thread mode
|
||||||
'manga_mode_active', 'unwanted_keywords', 'manga_filename_style',
|
'manga_mode_active', 'unwanted_keywords', 'manga_filename_style', 'scan_content_for_images', # Added scan_content_for_images
|
||||||
'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
|
||||||
]
|
]
|
||||||
args_template['skip_current_file_flag'] = None
|
args_template['skip_current_file_flag'] = None
|
||||||
@@ -3365,15 +3566,15 @@ class DownloaderApp(QWidget):
|
|||||||
'downloaded_files_lock', 'downloaded_file_hashes_lock', 'remove_from_filename_words_list', 'dynamic_character_filter_holder', # Added holder
|
'downloaded_files_lock', 'downloaded_file_hashes_lock', 'remove_from_filename_words_list', 'dynamic_character_filter_holder', # Added holder
|
||||||
'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', 'scan_content_for_images', # Added scan_content_for_images
|
||||||
'manga_mode_active', 'manga_filename_style',
|
'manga_mode_active', 'manga_filename_style', 'manga_date_prefix', # ADD manga_date_prefix
|
||||||
'manga_global_file_counter_ref' # Add new counter here
|
'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',
|
||||||
'show_external_links', 'extract_links_only', 'duplicate_file_mode', # Added duplicate_file_mode here
|
'show_external_links', 'extract_links_only', 'duplicate_file_mode', # Added duplicate_file_mode here
|
||||||
'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_prefix', # ADD manga_date_prefix
|
||||||
'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'
|
||||||
}
|
}
|
||||||
# Batching is generally for high worker counts.
|
# 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 is low (e.g., 1), the num_post_workers > POST_WORKER_BATCH_THRESHOLD condition will prevent batching.
|
||||||
@@ -3503,7 +3704,8 @@ class DownloaderApp(QWidget):
|
|||||||
self.manga_rename_toggle_button, # Visibility handled by update_ui_for_manga_mode
|
self.manga_rename_toggle_button, # Visibility handled by update_ui_for_manga_mode
|
||||||
self.cookie_browse_button, # Add cookie browse button
|
self.cookie_browse_button, # Add cookie browse button
|
||||||
self.multipart_toggle_button,
|
self.multipart_toggle_button,
|
||||||
self.cookie_text_input, # Add cookie text input
|
self.cookie_text_input, # Add cookie text input,
|
||||||
|
self.scan_content_images_checkbox, # Add scan content checkbox
|
||||||
self.use_cookie_checkbox, # Add cookie checkbox here
|
self.use_cookie_checkbox, # Add cookie checkbox here
|
||||||
self.external_links_checkbox
|
self.external_links_checkbox
|
||||||
]
|
]
|
||||||
@@ -3512,16 +3714,17 @@ class DownloaderApp(QWidget):
|
|||||||
all_potentially_toggleable_widgets = [
|
all_potentially_toggleable_widgets = [
|
||||||
self.link_input, self.dir_input, self.dir_button,
|
self.link_input, self.dir_input, self.dir_button,
|
||||||
self.page_range_label, self.start_page_input, self.to_label, self.end_page_input,
|
self.page_range_label, self.start_page_input, self.to_label, self.end_page_input,
|
||||||
self.character_input, self.char_filter_scope_toggle_button,
|
self.character_input, self.char_filter_scope_toggle_button, self.character_filter_widget, # Added character_filter_widget
|
||||||
|
self.filters_and_custom_folder_container_widget, # Added container
|
||||||
self.custom_folder_label, self.custom_folder_input,
|
self.custom_folder_label, self.custom_folder_input,
|
||||||
self.skip_words_input, self.skip_scope_toggle_button, self.remove_from_filename_input,
|
self.skip_words_input, self.skip_scope_toggle_button, self.remove_from_filename_input,
|
||||||
self.radio_all, self.radio_images, self.radio_videos, self.radio_only_archives, self.radio_only_links,
|
self.radio_all, self.radio_images, self.radio_videos, self.radio_only_archives, self.radio_only_links,
|
||||||
self.skip_zip_checkbox, self.skip_rar_checkbox, self.download_thumbnails_checkbox, self.compress_images_checkbox,
|
self.skip_zip_checkbox, self.skip_rar_checkbox, self.download_thumbnails_checkbox, self.compress_images_checkbox,
|
||||||
self.use_subfolders_checkbox, self.use_subfolder_per_post_checkbox,
|
self.use_subfolders_checkbox, self.use_subfolder_per_post_checkbox, self.scan_content_images_checkbox, # Added scan_content_images_checkbox
|
||||||
self.use_multithreading_checkbox, self.thread_count_input, self.thread_count_label,
|
self.use_multithreading_checkbox, self.thread_count_input, self.thread_count_label,
|
||||||
self.external_links_checkbox, self.manga_mode_checkbox, self.manga_rename_toggle_button, self.use_cookie_checkbox, self.cookie_text_input, self.cookie_browse_button,
|
self.external_links_checkbox, self.manga_mode_checkbox, self.manga_rename_toggle_button, self.use_cookie_checkbox, self.cookie_text_input, self.cookie_browse_button,
|
||||||
self.multipart_toggle_button,
|
self.multipart_toggle_button, self.radio_only_audio, # Added radio_only_audio
|
||||||
self.character_search_input, self.new_char_input, self.add_char_button, self.delete_char_button,
|
self.character_search_input, self.new_char_input, self.add_char_button, self.add_to_filter_button, self.delete_char_button, # Added add_to_filter_button
|
||||||
self.reset_button
|
self.reset_button
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3542,9 +3745,10 @@ class DownloaderApp(QWidget):
|
|||||||
if self.external_links_checkbox:
|
if self.external_links_checkbox:
|
||||||
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_ext_links = enabled and not is_only_links and not is_only_archives
|
is_only_audio = hasattr(self, 'radio_only_audio') and self.radio_only_audio.isChecked()
|
||||||
|
can_enable_ext_links = enabled and not is_only_links and not is_only_archives and not is_only_audio
|
||||||
self.external_links_checkbox.setEnabled(can_enable_ext_links)
|
self.external_links_checkbox.setEnabled(can_enable_ext_links)
|
||||||
if self.is_paused and not is_only_links and not is_only_archives:
|
if self.is_paused and not is_only_links and not is_only_archives and not is_only_audio:
|
||||||
self.external_links_checkbox.setEnabled(True)
|
self.external_links_checkbox.setEnabled(True)
|
||||||
if hasattr(self, 'use_cookie_checkbox'):
|
if hasattr(self, 'use_cookie_checkbox'):
|
||||||
self.use_cookie_checkbox.setEnabled(enabled or self.is_paused)
|
self.use_cookie_checkbox.setEnabled(enabled or self.is_paused)
|
||||||
@@ -3607,6 +3811,7 @@ class DownloaderApp(QWidget):
|
|||||||
self.skip_zip_checkbox.setChecked(True); self.skip_rar_checkbox.setChecked(True); self.download_thumbnails_checkbox.setChecked(False);
|
self.skip_zip_checkbox.setChecked(True); self.skip_rar_checkbox.setChecked(True); self.download_thumbnails_checkbox.setChecked(False);
|
||||||
self.compress_images_checkbox.setChecked(False); self.use_subfolders_checkbox.setChecked(True);
|
self.compress_images_checkbox.setChecked(False); self.use_subfolders_checkbox.setChecked(True);
|
||||||
self.use_subfolder_per_post_checkbox.setChecked(False); self.use_multithreading_checkbox.setChecked(True);
|
self.use_subfolder_per_post_checkbox.setChecked(False); self.use_multithreading_checkbox.setChecked(True);
|
||||||
|
if hasattr(self, 'scan_content_images_checkbox'): self.scan_content_images_checkbox.setChecked(False) # Reset new checkbox
|
||||||
self.external_links_checkbox.setChecked(False)
|
self.external_links_checkbox.setChecked(False)
|
||||||
if self.manga_mode_checkbox: self.manga_mode_checkbox.setChecked(False)
|
if self.manga_mode_checkbox: self.manga_mode_checkbox.setChecked(False)
|
||||||
if hasattr(self, 'use_cookie_checkbox'): self.use_cookie_checkbox.setChecked(self.use_cookie_setting) # Reset to loaded or False
|
if hasattr(self, 'use_cookie_checkbox'): self.use_cookie_checkbox.setChecked(self.use_cookie_setting) # Reset to loaded or False
|
||||||
@@ -3619,7 +3824,9 @@ class DownloaderApp(QWidget):
|
|||||||
self.skip_words_scope = SKIP_SCOPE_POSTS # Default
|
self.skip_words_scope = SKIP_SCOPE_POSTS # Default
|
||||||
self._update_skip_scope_button_text()
|
self._update_skip_scope_button_text()
|
||||||
|
|
||||||
self.char_filter_scope = CHAR_SCOPE_TITLE # Default
|
if hasattr(self, 'manga_date_prefix_input'): self.manga_date_prefix_input.clear() # Clear prefix input
|
||||||
|
|
||||||
|
self.char_filter_scope = CHAR_SCOPE_FILES # Default to Files on soft reset
|
||||||
self._update_char_filter_scope_button_text()
|
self._update_char_filter_scope_button_text()
|
||||||
|
|
||||||
self.manga_filename_style = STYLE_POST_TITLE # Reset to app default
|
self.manga_filename_style = STYLE_POST_TITLE # Reset to app default
|
||||||
@@ -3753,6 +3960,24 @@ class DownloaderApp(QWidget):
|
|||||||
|
|
||||||
self.set_ui_enabled(True) # Full UI reset if not retrying
|
self.set_ui_enabled(True) # Full UI reset if not retrying
|
||||||
|
|
||||||
|
def _handle_thumbnail_mode_change(self, thumbnails_checked):
|
||||||
|
"""Handles UI changes when 'Download Thumbnails Only' is toggled."""
|
||||||
|
if not hasattr(self, 'scan_content_images_checkbox'):
|
||||||
|
return
|
||||||
|
|
||||||
|
if thumbnails_checked:
|
||||||
|
self.scan_content_images_checkbox.setChecked(True)
|
||||||
|
self.scan_content_images_checkbox.setEnabled(False)
|
||||||
|
self.scan_content_images_checkbox.setToolTip(
|
||||||
|
"Automatically enabled and locked because 'Download Thumbnails Only' is active.\n"
|
||||||
|
"In this mode, only images found by content scanning will be downloaded."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.scan_content_images_checkbox.setEnabled(True)
|
||||||
|
# Revert to unchecked when thumbnail mode is off. User can manually re-check if desired.
|
||||||
|
self.scan_content_images_checkbox.setChecked(False)
|
||||||
|
self.scan_content_images_checkbox.setToolTip(self._original_scan_content_tooltip)
|
||||||
|
|
||||||
def _start_failed_files_retry_session(self):
|
def _start_failed_files_retry_session(self):
|
||||||
self.log_signal.emit(f"🔄 Starting retry session for {len(self.retryable_failed_files_info)} file(s)...")
|
self.log_signal.emit(f"🔄 Starting retry session for {len(self.retryable_failed_files_info)} file(s)...")
|
||||||
self.set_ui_enabled(False) # Disable UI, but cancel button will be enabled
|
self.set_ui_enabled(False) # Disable UI, but cancel button will be enabled
|
||||||
@@ -3826,6 +4051,12 @@ class DownloaderApp(QWidget):
|
|||||||
'api_url_input': job_details.get('api_url_input', ''), # Original post's API URL
|
'api_url_input': job_details.get('api_url_input', ''), # Original post's API URL
|
||||||
'manga_mode_active': job_details.get('manga_mode_active_for_file', False),
|
'manga_mode_active': job_details.get('manga_mode_active_for_file', False),
|
||||||
'manga_filename_style': job_details.get('manga_filename_style_for_file', STYLE_POST_TITLE),
|
'manga_filename_style': job_details.get('manga_filename_style_for_file', STYLE_POST_TITLE),
|
||||||
|
# Ensure scan_content_for_images is passed if it's part of common_args or needed
|
||||||
|
'scan_content_for_images': common_args.get('scan_content_for_images', False),
|
||||||
|
'use_cookie': common_args.get('use_cookie', False),
|
||||||
|
'cookie_text': common_args.get('cookie_text', ""),
|
||||||
|
'selected_cookie_file': common_args.get('selected_cookie_file', None),
|
||||||
|
'app_base_dir': common_args.get('app_base_dir', None),
|
||||||
}
|
}
|
||||||
worker = PostProcessorWorker(**ppw_init_args)
|
worker = PostProcessorWorker(**ppw_init_args)
|
||||||
|
|
||||||
@@ -3965,6 +4196,7 @@ class DownloaderApp(QWidget):
|
|||||||
self.missed_title_key_terms_examples.clear()
|
self.missed_title_key_terms_examples.clear()
|
||||||
self.logged_summary_for_key_term.clear()
|
self.logged_summary_for_key_term.clear()
|
||||||
self.already_logged_bold_key_terms.clear()
|
self.already_logged_bold_key_terms.clear()
|
||||||
|
if hasattr(self, 'manga_date_prefix_input'): self.manga_date_prefix_input.clear() # Clear prefix input
|
||||||
if self.pause_event: self.pause_event.clear()
|
if self.pause_event: self.pause_event.clear()
|
||||||
self.is_paused = False # Reset pause state
|
self.is_paused = False # Reset pause state
|
||||||
self.missed_key_terms_buffer.clear()
|
self.missed_key_terms_buffer.clear()
|
||||||
@@ -3994,10 +4226,16 @@ class DownloaderApp(QWidget):
|
|||||||
self.log_verbosity_toggle_button.setToolTip("Current View: Progress Log. Click to switch to Missed Character Log.")
|
self.log_verbosity_toggle_button.setToolTip("Current View: Progress Log. Click to switch to Missed Character Log.")
|
||||||
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)
|
||||||
|
# Ensure scan_content_images_checkbox is reset and its state updated by thumbnail mode
|
||||||
|
if hasattr(self, 'scan_content_images_checkbox'):
|
||||||
|
self.scan_content_images_checkbox.setChecked(False)
|
||||||
|
if hasattr(self, 'download_thumbnails_checkbox'):
|
||||||
|
self._handle_thumbnail_mode_change(self.download_thumbnails_checkbox.isChecked())
|
||||||
|
|
||||||
def _show_feature_guide(self):
|
def _show_feature_guide(self):
|
||||||
# Define content for each page
|
# Define content for each page
|
||||||
page1_title = "① Introduction & Main Inputs"
|
page1_title = "① Introduction & Main Inputs"
|
||||||
|
|
||||||
page1_content = """<html><head/><body>
|
page1_content = """<html><head/><body>
|
||||||
<p>This guide provides an overview of the Kemono Downloader's features, fields, and buttons.</p>
|
<p>This guide provides an overview of the Kemono Downloader's features, fields, and buttons.</p>
|
||||||
|
|
||||||
@@ -4076,6 +4314,7 @@ class DownloaderApp(QWidget):
|
|||||||
<li><code>Images/GIFs</code>: Only common image formats (JPG, PNG, GIF, WEBP, etc.) and GIFs.</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>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 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 Audio</code>: Downloads only common audio formats (MP3, WAV, FLAC, M4A, OGG, etc.). Other file-specific options behave as with 'Images' or 'Videos' mode.</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>
|
<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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@@ -4125,12 +4364,14 @@ class DownloaderApp(QWidget):
|
|||||||
<li>The 'Page Range' input is disabled as all posts are fetched.</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:
|
<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>
|
<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: Post Title (Default)</code>: The first file in a post is named after the post's cleaned title (e.g., 'My Chapter 1.jpg'). Subsequent files within the *same post* will attempt to keep their original filenames (e.g., 'page_02.png', 'bonus_art.jpg'). If the post has only one file, it's named after the post title. This is generally recommended for most manga/comics.</li>
|
||||||
<li><code>Name: Original File</code>: All files attempt to keep their original filenames.</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>
|
<li><code>Name: Original File</code>: All files attempt to keep their original filenames. When this style is active, an input field for an <b>optional filename prefix</b> (e.g., 'MySeries_') will appear next to this style button. Example: 'MySeries_OriginalFile.jpg'.</li>
|
||||||
|
<li><code>Name: Title+G.Num (Post Title + Global Numbering)</code>: All files across all posts in the current download session are named sequentially using the post's cleaned title as a prefix, followed by a global counter. Example: Post 'Chapter 1' (2 files) -> 'Chapter 1 001.jpg', 'Chapter 1 002.png'. Next post 'Chapter 2' (1 file) -> 'Chapter 2 003.jpg'. Multithreading for post processing is automatically disabled for this style.</li>
|
||||||
|
<li><code>Name: Date Based</code>: Files are named sequentially (001.ext, 002.ext, ...) based on post publication order. When this style is active, an input field for an <b>optional filename prefix</b> (e.g., 'MySeries_') will appear next to this style button. Example: 'MySeries_001.jpg'. Multithreading for post processing is automatically disabled for this style.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</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>
|
<li>For best results with 'Name: Post Title', 'Name: Title+G.Num', or 'Name: Date Based' styles, use the 'Filter by Character(s)' field with the manga/series title for folder organization.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul></li></ul>
|
</ul></li></ul>
|
||||||
@@ -4158,6 +4399,18 @@ class DownloaderApp(QWidget):
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</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>➕ Add Button:</b> Adds the name/group from the input field above to the list and <code>Known.txt</code>.</li>
|
||||||
|
<li><b>⤵️ Add to Filter Button:</b>
|
||||||
|
<ul>
|
||||||
|
<li>Located next to the '➕ Add' button for the 'Known Shows/Characters' list.</li>
|
||||||
|
<li>Clicking this button opens a popup window displaying all names from your <code>Known.txt</code> file, each with a checkbox.</li>
|
||||||
|
<li>The popup includes a search bar to quickly filter the list of names.</li>
|
||||||
|
<li>You can select one or more names using the checkboxes.</li>
|
||||||
|
<li>Click 'Add Selected' to insert the chosen names into the 'Filter by Character(s)' input field in the main window.</li>
|
||||||
|
<li>If a selected name from <code>Known.txt</code> was originally a group (e.g., defined as <code>(Boa, Hancock)</code> in Known.txt), it will be added to the filter field as <code>(Boa, Hancock)~</code>. Simple names are added as-is.</li>
|
||||||
|
<li>'Select All' and 'Deselect All' buttons are available in the popup for convenience.</li>
|
||||||
|
<li>Click 'Cancel' to close the popup without any changes.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li><b>🗑️ Delete Selected Button:</b> Deletes the selected name(s) from 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>
|
<li><b>❓ Button (This one!):</b> Displays this comprehensive help guide.</li>
|
||||||
</ul></body></html>"""
|
</ul></body></html>"""
|
||||||
@@ -4173,6 +4426,7 @@ class DownloaderApp(QWidget):
|
|||||||
<li><b>Name: [Style] Button (Manga Filename Style):</b>
|
<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>
|
<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>
|
<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>
|
||||||
|
<li>When 'Original File' or 'Date Based' style is active, an input field for an <b>optional filename prefix</b> will appear next to this button.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><b>Multi-part: [ON/OFF] Button:</b>
|
<li><b>Multi-part: [ON/OFF] Button:</b>
|
||||||
@@ -4348,6 +4602,46 @@ class DownloaderApp(QWidget):
|
|||||||
QMessageBox.critical(self, "Error Opening File", f"Could not open '{os.path.basename(self.config_file)}':\n{e}")
|
QMessageBox.critical(self, "Error Opening File", f"Could not open '{os.path.basename(self.config_file)}':\n{e}")
|
||||||
self.log_signal.emit(f"❌ Error opening '{os.path.basename(self.config_file)}': {e}")
|
self.log_signal.emit(f"❌ Error opening '{os.path.basename(self.config_file)}': {e}")
|
||||||
|
|
||||||
|
def _show_add_to_filter_dialog(self):
|
||||||
|
global KNOWN_NAMES
|
||||||
|
if not KNOWN_NAMES:
|
||||||
|
QMessageBox.information(self, "No Known Names", "Your 'Known.txt' list is empty. Add some names first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog = KnownNamesFilterDialog(KNOWN_NAMES, self)
|
||||||
|
if dialog.exec_() == QDialog.Accepted:
|
||||||
|
selected_entries = dialog.get_selected_entries() # Get list of entry objects
|
||||||
|
if selected_entries:
|
||||||
|
self._add_names_to_character_filter_input(selected_entries)
|
||||||
|
|
||||||
|
def _add_names_to_character_filter_input(self, selected_entries_list):
|
||||||
|
current_filter_text = self.character_input.text().strip()
|
||||||
|
|
||||||
|
# Split existing text by comma, trim, and filter out empty strings
|
||||||
|
existing_filter_parts = [part.strip() for part in current_filter_text.split(',') if part.strip()]
|
||||||
|
|
||||||
|
# Use a set for efficient checking of existing parts (case-insensitive)
|
||||||
|
existing_parts_lower_set = {part.lower() for part in existing_filter_parts}
|
||||||
|
|
||||||
|
newly_added_parts_for_field = []
|
||||||
|
for entry_obj in selected_entries_list:
|
||||||
|
text_for_field = ""
|
||||||
|
if entry_obj.get('is_group', False):
|
||||||
|
# For groups from Known.txt, format as (alias1, alias2)~
|
||||||
|
# entry_obj['aliases'] should contain the original terms like 'Boa', 'Hancock'
|
||||||
|
aliases_str = ", ".join(sorted(entry_obj.get('aliases', []), key=str.lower))
|
||||||
|
text_for_field = f"({aliases_str})~"
|
||||||
|
else:
|
||||||
|
text_for_field = entry_obj['name']
|
||||||
|
|
||||||
|
if text_for_field.lower() not in existing_parts_lower_set:
|
||||||
|
newly_added_parts_for_field.append(text_for_field)
|
||||||
|
existing_parts_lower_set.add(text_for_field.lower())
|
||||||
|
|
||||||
|
final_filter_parts = existing_filter_parts + newly_added_parts_for_field
|
||||||
|
self.character_input.setText(", ".join(final_filter_parts))
|
||||||
|
self.log_signal.emit(f"ℹ️ Added to filter: {', '.join(newly_added_parts_for_field)}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import traceback
|
import traceback
|
||||||
import sys # Ensure sys is imported here if not already
|
import sys # Ensure sys is imported here if not already
|
||||||
|
|||||||
69
readme.md
69
readme.md
@@ -1,4 +1,4 @@
|
|||||||
<h1 align="center">Kemono Downloader v4.0.0</h1>
|
<h1 align="center">Kemono Downloader v4.1.1</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,9 +11,33 @@ Built with **PyQt5**, this tool is ideal for users who want deep filtering, cust
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What's New in v4.0.0?
|
## What's New in v4.1.1?
|
||||||
|
|
||||||
Version 3.5.0 focuses on enhancing access to content and providing even smarter organization:
|
Version 4.1.1 introduces a smarter way to capture images that might be embedded directly within post descriptions, enhancing content discovery.
|
||||||
|
|
||||||
|
### "Scan Content for Images" Feature
|
||||||
|
|
||||||
|
- **Enhanced Image Discovery:** A new checkbox, "**Scan Content for Images**," has been added to the UI (grouped with "Download Thumbnails Only" and "Compress Large Images").
|
||||||
|
- **How it Works:**
|
||||||
|
- When enabled, the downloader scans the HTML content of posts (e.g., the description area).
|
||||||
|
- It looks for images embedded via HTML `<img>` tags or as direct absolute URL links (e.g., `https://.../image.png`).
|
||||||
|
- It intelligently resolves relative image paths found in `<img>` tags (like `/data/image.jpg`) into full, downloadable URLs.
|
||||||
|
- This is particularly useful for capturing images that are part of the post's narrative but not formally listed in the API's file or attachment sections.
|
||||||
|
- **Default State:** This option is **unchecked by default**.
|
||||||
|
- **Interaction with "Download Thumbnails Only":**
|
||||||
|
- If you check "Download Thumbnails Only":
|
||||||
|
- The "Scan Content for Images" checkbox will **automatically become checked and disabled** (locked).
|
||||||
|
- In this combined mode, the downloader will **only download images found by the content scan**. API-listed thumbnails will be ignored, prioritizing images from the post's body.
|
||||||
|
- If you uncheck "Download Thumbnails Only":
|
||||||
|
- The "Scan Content for Images" checkbox will become **enabled again and revert to being unchecked**. You can then manually enable it if you wish to scan content without being in thumbnail-only mode.
|
||||||
|
|
||||||
|
This feature ensures a more comprehensive download experience, especially for posts where images are integrated directly into the text.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previous Update: What's New in v4.0.1?
|
||||||
|
|
||||||
|
Version 4.0.1 focuses on enhancing access to content and providing even smarter organization:
|
||||||
|
|
||||||
### Cookie Management
|
### Cookie Management
|
||||||
|
|
||||||
@@ -71,13 +95,30 @@ This field allows for dynamic filtering for the current download session and pro
|
|||||||
- **Adding New Names from Filters:** When you use the "Filter by Character(s)" input, if any names or groups are new (not already in `Known.txt`), a dialog will appear after you start the download. This dialog allows you to select which of these new names/groups should be added to `Known.txt`, formatted according to the rules described above.
|
- **Adding New Names from Filters:** When you use the "Filter by Character(s)" input, if any names or groups are new (not already in `Known.txt`), a dialog will appear after you start the download. This dialog allows you to select which of these new names/groups should be added to `Known.txt`, formatted according to the rules described above.
|
||||||
- **Intelligent Fallback:** If "Separate Folders by Name/Title" is active, and content doesn't match the "Filter by Character(s)" UI input, the downloader consults your `Known.txt` file for folder naming.
|
- **Intelligent Fallback:** If "Separate Folders by Name/Title" is active, and content doesn't match the "Filter by Character(s)" UI input, the downloader consults your `Known.txt` file for folder naming.
|
||||||
- **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.
|
||||||
|
- **Using Known Names to Populate Filters (via "Add to Filter" Button):**
|
||||||
|
- Next to the "Add" button in the `Known.txt` management section, a "⤵️ Add to Filter" button provides a quick way to use your existing known names.
|
||||||
|
- Clicking this opens a popup window displaying all entries from your `Known.txt` file, each with a checkbox.
|
||||||
|
- The popup includes:
|
||||||
|
- A search bar to quickly filter the list of names.
|
||||||
|
- "Select All" and "Deselect All" buttons for convenience.
|
||||||
|
- After selecting the desired names, click "Add Selected".
|
||||||
|
- The chosen names will be inserted into the "Filter by Character(s)" input field.
|
||||||
|
- **Important Formatting:** If a selected entry from `Known.txt` is a group (e.g., originally `(Boa Hancock)` in `Known.txt`, which implies aliases "Boa" and "Hancock"), it will be added to the filter field as `(Boa, Hancock)~`. Simple names are added as-is.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
## What's in v3.5.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 brought significant enhancements to manga/comic downloading, filtering capabilities, and user experience:
|
||||||
|
|
||||||
### Enhanced Manga/Comic Mode
|
### Enhanced Manga/Comic Mode
|
||||||
|
|
||||||
|
- **Optional Filename Prefix:**
|
||||||
|
- When using the "Date Based" or "Original File Name" manga styles, an optional prefix can be specified in the UI.
|
||||||
|
- This prefix will be prepended to each filename generated by these styles.
|
||||||
|
- **Example (Date Based):** If prefix is `MySeries_`, files become `MySeries_001.jpg`, `MySeries_002.png`, etc.
|
||||||
|
- **Example (Original File Name):** If prefix is `Comic_Vol1_`, an original file `page_01.jpg` becomes `Comic_Vol1_page_01.jpg`.
|
||||||
|
- This input field appears automatically when either of these two manga naming styles is selected.
|
||||||
|
|
||||||
- **New "Date Based" Filename Style:**
|
- **New "Date Based" Filename Style:**
|
||||||
|
|
||||||
- Perfect for truly sequential content! Files are named numerically (e.g., `001.jpg`, `002.jpg`, `003.ext`...) across an *entire creator's feed*, strictly following post publication order.
|
- Perfect for truly sequential content! Files are named numerically (e.g., `001.jpg`, `002.jpg`, `003.ext`...) across an *entire creator's feed*, strictly following post publication order.
|
||||||
@@ -87,6 +128,13 @@ This version brings significant enhancements to manga/comic downloading, filteri
|
|||||||
- **Guaranteed Order:** Disables multi-threading for post processing to ensure sequential accuracy.
|
- **Guaranteed Order:** Disables multi-threading for post processing to ensure sequential accuracy.
|
||||||
|
|
||||||
- Works alongside the existing "Post Title" and "Original File Name" styles.
|
- Works alongside the existing "Post Title" and "Original File Name" styles.
|
||||||
|
- **New "Title+G.Num (Post Title + Global Numbering)" Filename Style:**
|
||||||
|
- Ideal for series where you want each file to be prefixed by its post title but still maintain a global sequential number across all posts from a single download session.
|
||||||
|
- **Naming Convention:** Files are named using the cleaned post title as a prefix, followed by an underscore and a globally incrementing number (e.g., `Post Title_001.ext`, `Post Title_002.ext`).
|
||||||
|
- **Example:**
|
||||||
|
- Post "Chapter 1: The Adventure Begins" (contains 2 files: `imageA.jpg`, `imageB.png`) -> `Chapter 1 The Adventure Begins_001.jpg`, `Chapter 1 The Adventure Begins_002.png`
|
||||||
|
- Next Post "Chapter 2: New Friends" (contains 1 file: `cover.jpg`) -> `Chapter 2 New Friends_003.jpg`
|
||||||
|
- **Sequential Integrity:** Multithreading for post processing is automatically disabled when this style is selected to ensure the global numbering is strictly sequential.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -169,6 +217,7 @@ This version brings significant enhancements to manga/comic downloading, filteri
|
|||||||
- `Nami` (simple character)
|
- `Nami` (simple character)
|
||||||
- `(Boa Hancock)~` (aliases for one character, session folder "Boa Hancock", adds `(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`)
|
||||||
|
- A "⤵️ Add to Filter" button (near the `Known.txt` management UI) allows you to quickly populate this field by selecting from your existing `Known.txt` entries via a popup with search and checkbox selection.
|
||||||
- See "Advanced `Known.txt` and Character Filtering" for full details.
|
- See "Advanced `Known.txt` and Character Filtering" for full details.
|
||||||
- **Filter Scopes:**
|
- **Filter Scopes:**
|
||||||
- `Files`
|
- `Files`
|
||||||
@@ -200,6 +249,7 @@ This version brings significant enhancements to manga/comic downloading, filteri
|
|||||||
- `Name: Post Title (Default)`
|
- `Name: Post Title (Default)`
|
||||||
- `Name: Original File`
|
- `Name: Original File`
|
||||||
- `Name: Date Based (New)`
|
- `Name: Date Based (New)`
|
||||||
|
- `Name: Title+G.Num (Post Title + Global Numbering)`
|
||||||
|
|
||||||
- **Best With:** Character filters set to manga/series title
|
- **Best With:** Character filters set to manga/series title
|
||||||
|
|
||||||
@@ -217,12 +267,17 @@ This version brings significant enhancements to manga/comic downloading, filteri
|
|||||||
---
|
---
|
||||||
|
|
||||||
### Thumbnail & Compression Tools
|
### Thumbnail & Compression Tools
|
||||||
|
- **Download Thumbnails Only:**
|
||||||
- **Download Thumbnails Only**
|
- Downloads small preview images from the API instead of full-sized files (if available).
|
||||||
|
- **Interaction with "Scan Content for Images" (New in v4.1.1):** When "Download Thumbnails Only" is active, "Scan Content for Images" is auto-enabled, and only images found by the content scan are downloaded. See "What's New in v4.1.1" for details.
|
||||||
|
- **Scan Content for Images (New in v4.1.1):**
|
||||||
|
- A UI option to scan the HTML content of posts for embedded image URLs (from `<img>` tags or direct links).
|
||||||
|
- Resolves relative paths and helps capture images not listed in the API's formal attachments.
|
||||||
|
- See the "What's New in v4.1.1?" section for a comprehensive explanation.
|
||||||
- **Compress to WebP** (via Pillow)
|
- **Compress to WebP** (via Pillow)
|
||||||
- Converts large images to smaller WebP versions
|
- Converts large images to smaller WebP versions
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Performance Features
|
### Performance Features
|
||||||
|
|||||||
Reference in New Issue
Block a user