mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-29 16:14:44 +00:00
Compare commits
No commits in common. "main" and "v7.9.1" have entirely different histories.
@ -15,6 +15,7 @@ from ..config.constants import (
|
|||||||
STYLE_POST_TITLE_GLOBAL_NUMBERING
|
STYLE_POST_TITLE_GLOBAL_NUMBERING
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- NEW: Custom Adapter to fix SSL errors ---
|
||||||
class CustomSSLAdapter(HTTPAdapter):
|
class CustomSSLAdapter(HTTPAdapter):
|
||||||
"""
|
"""
|
||||||
A custom HTTPAdapter that forces check_hostname=False when using SSL.
|
A custom HTTPAdapter that forces check_hostname=False when using SSL.
|
||||||
@ -129,13 +130,13 @@ def fetch_single_post_data(api_domain, service, user_id, post_id, headers, logge
|
|||||||
|
|
||||||
response = scraper.get(post_api_url, headers=headers, timeout=request_timeout, cookies=cookies_dict, proxies=proxies, verify=False)
|
response = scraper.get(post_api_url, headers=headers, timeout=request_timeout, cookies=cookies_dict, proxies=proxies, verify=False)
|
||||||
|
|
||||||
|
# --- FIX: Handle 429 Rate Limit explicitly ---
|
||||||
if response.status_code == 429:
|
if response.status_code == 429:
|
||||||
wait_time = 20 + (attempt * 10) # 20s, 30s, 40s...
|
wait_time = 20 + (attempt * 10) # 20s, 30s, 40s...
|
||||||
logger(f" ⚠️ Rate Limited (429) on post {post_id}. Waiting {wait_time} seconds before retrying...")
|
logger(f" ⚠️ Rate Limited (429) on post {post_id}. Waiting {wait_time} seconds before retrying...")
|
||||||
time.sleep(wait_time)
|
time.sleep(wait_time)
|
||||||
continue # Try loop again
|
continue # Try loop again
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
@ -265,9 +266,11 @@ def download_from_api(
|
|||||||
if target_post_id and (start_page or end_page):
|
if target_post_id and (start_page or end_page):
|
||||||
logger("⚠️ Page range (start/end page) is ignored when a specific post URL is provided (searching all pages for the post).")
|
logger("⚠️ Page range (start/end page) is ignored when a specific post URL is provided (searching all pages for the post).")
|
||||||
|
|
||||||
|
# --- FIXED LOGIC HERE ---
|
||||||
|
# Define which styles require fetching ALL posts first (Sequential Mode)
|
||||||
styles_requiring_fetch_all = [STYLE_DATE_BASED, STYLE_POST_TITLE_GLOBAL_NUMBERING]
|
styles_requiring_fetch_all = [STYLE_DATE_BASED, STYLE_POST_TITLE_GLOBAL_NUMBERING]
|
||||||
|
|
||||||
|
# Only enable "fetch all and sort" if the current style is explicitly in the list above
|
||||||
is_manga_mode_fetch_all_and_sort_oldest_first = (
|
is_manga_mode_fetch_all_and_sort_oldest_first = (
|
||||||
manga_mode and
|
manga_mode and
|
||||||
(manga_filename_style_for_sort_check in styles_requiring_fetch_all) and
|
(manga_filename_style_for_sort_check in styles_requiring_fetch_all) and
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import requests
|
import requests
|
||||||
import re
|
import re
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup # Optional, but regex is faster for this specific site
|
||||||
|
|
||||||
|
|
||||||
|
# Logic derived from NHdownloader.sh 'hentaifox' function
|
||||||
BASE_URL = "https://hentaifox.com"
|
BASE_URL = "https://hentaifox.com"
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||||
@ -25,10 +25,11 @@ def get_gallery_metadata(gallery_id):
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
html = response.text
|
html = response.text
|
||||||
|
|
||||||
|
# Extract Title (Bash: grep -o '<title>.*</title>')
|
||||||
title_match = re.search(r'<title>(.*?)</title>', html)
|
title_match = re.search(r'<title>(.*?)</title>', html)
|
||||||
title = title_match.group(1).replace(" - HentaiFox", "").strip() if title_match else f"Gallery {gallery_id}"
|
title = title_match.group(1).replace(" - HentaiFox", "").strip() if title_match else f"Gallery {gallery_id}"
|
||||||
|
|
||||||
|
# Extract Total Pages (Bash: grep -Eo 'Pages: [0-9]*')
|
||||||
pages_match = re.search(r'Pages: (\d+)', html)
|
pages_match = re.search(r'Pages: (\d+)', html)
|
||||||
if not pages_match:
|
if not pages_match:
|
||||||
raise ValueError("Could not find total pages count.")
|
raise ValueError("Could not find total pages count.")
|
||||||
|
|||||||
@ -686,6 +686,7 @@ class PostProcessorWorker:
|
|||||||
response = requests.get(new_url, headers=file_download_headers, timeout=(30, 300), stream=True, cookies=cookies_to_use_for_file, proxies=self.proxies, verify=False)
|
response = requests.get(new_url, headers=file_download_headers, timeout=(30, 300), stream=True, cookies=cookies_to_use_for_file, proxies=self.proxies, verify=False)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# --- REVISED AND MOVED SIZE CHECK LOGIC ---
|
||||||
total_size_bytes = int(response.headers.get('Content-Length', 0))
|
total_size_bytes = int(response.headers.get('Content-Length', 0))
|
||||||
|
|
||||||
if self.skip_file_size_mb is not None:
|
if self.skip_file_size_mb is not None:
|
||||||
@ -694,7 +695,8 @@ class PostProcessorWorker:
|
|||||||
if file_size_mb < self.skip_file_size_mb:
|
if file_size_mb < self.skip_file_size_mb:
|
||||||
self.logger(f" -> Skip File (Size): '{api_original_filename}' is {file_size_mb:.2f} MB, which is smaller than the {self.skip_file_size_mb} MB limit.")
|
self.logger(f" -> Skip File (Size): '{api_original_filename}' is {file_size_mb:.2f} MB, which is smaller than the {self.skip_file_size_mb} MB limit.")
|
||||||
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
|
||||||
|
# If Content-Length is missing, we can't check, so we no longer log a warning here and just proceed.
|
||||||
|
# --- END OF REVISED LOGIC ---
|
||||||
|
|
||||||
num_parts_for_file = min(self.multipart_parts_count, MAX_PARTS_FOR_MULTIPART_DOWNLOAD)
|
num_parts_for_file = min(self.multipart_parts_count, MAX_PARTS_FOR_MULTIPART_DOWNLOAD)
|
||||||
|
|
||||||
|
|||||||
@ -75,6 +75,11 @@ def add_metadata_page(pdf, post, font_family):
|
|||||||
if link_url:
|
if link_url:
|
||||||
# Styling for clickable link: Blue + Underline
|
# Styling for clickable link: Blue + Underline
|
||||||
pdf.set_text_color(0, 0, 255)
|
pdf.set_text_color(0, 0, 255)
|
||||||
|
# Check if font supports underline style directly or just use 'U'
|
||||||
|
# FPDF standard allows 'U' in style string.
|
||||||
|
# We use 'U' combined with the font family.
|
||||||
|
# Note: DejaVu implementation in fpdf2 might handle 'U' automatically or ignore it depending on version,
|
||||||
|
# but setting text color indicates link clearly enough usually.
|
||||||
pdf.set_font(font_family, 'U', 11)
|
pdf.set_font(font_family, 'U', 11)
|
||||||
|
|
||||||
# Pass the URL to the 'link' parameter
|
# Pass the URL to the 'link' parameter
|
||||||
@ -126,9 +131,9 @@ def create_individual_pdf(post_data, output_filename, font_path, add_info_page=F
|
|||||||
font_family = _setup_pdf_fonts(pdf, font_path, logger)
|
font_family = _setup_pdf_fonts(pdf, font_path, logger)
|
||||||
|
|
||||||
if add_info_page:
|
if add_info_page:
|
||||||
|
# add_metadata_page adds the page start itself
|
||||||
add_metadata_page(pdf, post_data, font_family)
|
add_metadata_page(pdf, post_data, font_family)
|
||||||
|
# REMOVED: pdf.add_page() <-- This ensures content starts right below the line
|
||||||
else:
|
else:
|
||||||
pdf.add_page()
|
pdf.add_page()
|
||||||
|
|
||||||
@ -205,6 +210,7 @@ def create_single_pdf_from_content(posts_data, output_filename, font_path, add_i
|
|||||||
for i, post in enumerate(posts_data):
|
for i, post in enumerate(posts_data):
|
||||||
if add_info_page:
|
if add_info_page:
|
||||||
add_metadata_page(pdf, post, font_family)
|
add_metadata_page(pdf, post, font_family)
|
||||||
|
# REMOVED: pdf.add_page() <-- This ensures content starts right below the line
|
||||||
else:
|
else:
|
||||||
pdf.add_page()
|
pdf.add_page()
|
||||||
|
|
||||||
|
|||||||
@ -6636,11 +6636,11 @@ class DownloaderApp (QWidget ):
|
|||||||
# Look up the name in the cache, falling back to the ID if not found.
|
# Look up the name in the cache, falling back to the ID if not found.
|
||||||
creator_name = self.creator_name_cache.get((service, user_id), user_id)
|
creator_name = self.creator_name_cache.get((service, user_id), user_id)
|
||||||
|
|
||||||
|
# Add the new 'creator_name' key to the format_values dictionary.
|
||||||
format_values = {
|
format_values = {
|
||||||
'id': str(job_details.get('original_post_id_for_log', '')),
|
'id': str(job_details.get('original_post_id_for_log', '')),
|
||||||
'user': user_id,
|
'user': user_id,
|
||||||
'creator_name': creator_name,
|
'creator_name': creator_name, # <-- ADDED
|
||||||
'service': str(job_details.get('service', '')),
|
'service': str(job_details.get('service', '')),
|
||||||
'title': post_title,
|
'title': post_title,
|
||||||
'name': base,
|
'name': base,
|
||||||
@ -7075,6 +7075,7 @@ class DownloaderApp (QWidget ):
|
|||||||
|
|
||||||
self.log_signal.emit(f" Fetched a total of {len(all_posts_from_api)} posts from the server.")
|
self.log_signal.emit(f" Fetched a total of {len(all_posts_from_api)} posts from the server.")
|
||||||
|
|
||||||
|
# CORRECTED LINE: Assign the list directly without re-filtering
|
||||||
self.new_posts_for_update = all_posts_from_api
|
self.new_posts_for_update = all_posts_from_api
|
||||||
|
|
||||||
if not self.new_posts_for_update:
|
if not self.new_posts_for_update:
|
||||||
@ -7102,6 +7103,7 @@ class DownloaderApp (QWidget ):
|
|||||||
self.log_signal.emit(f" Update session will save to base folder: {base_download_dir_from_ui}")
|
self.log_signal.emit(f" Update session will save to base folder: {base_download_dir_from_ui}")
|
||||||
|
|
||||||
raw_character_filters_text = self.character_input.text().strip()
|
raw_character_filters_text = self.character_input.text().strip()
|
||||||
|
# FIX: Parse both filters and commands from the input string
|
||||||
parsed_character_filter_objects, download_commands = self._parse_character_filters(raw_character_filters_text)
|
parsed_character_filter_objects, download_commands = self._parse_character_filters(raw_character_filters_text)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -7179,7 +7181,11 @@ class DownloaderApp (QWidget ):
|
|||||||
'single_pdf_mode': self.single_pdf_setting,
|
'single_pdf_mode': self.single_pdf_setting,
|
||||||
'project_root_dir': self.app_base_dir,
|
'project_root_dir': self.app_base_dir,
|
||||||
'processed_post_ids': list(self.active_update_profile['processed_post_ids']),
|
'processed_post_ids': list(self.active_update_profile['processed_post_ids']),
|
||||||
|
|
||||||
|
# FIX: Use the parsed commands dictionary to get the sfp_threshold
|
||||||
'sfp_threshold': download_commands.get('sfp_threshold'),
|
'sfp_threshold': download_commands.get('sfp_threshold'),
|
||||||
|
|
||||||
|
# FIX: Add all the missing keys
|
||||||
'date_prefix_format': self.date_prefix_format,
|
'date_prefix_format': self.date_prefix_format,
|
||||||
'domain_override': download_commands.get('domain_override'),
|
'domain_override': download_commands.get('domain_override'),
|
||||||
'archive_only_mode': download_commands.get('archive_only', False),
|
'archive_only_mode': download_commands.get('archive_only', False),
|
||||||
@ -7212,9 +7218,11 @@ class DownloaderApp (QWidget ):
|
|||||||
dialog = EmptyPopupDialog(self.user_data_path, self)
|
dialog = EmptyPopupDialog(self.user_data_path, self)
|
||||||
if dialog.exec_() == QDialog.Accepted:
|
if dialog.exec_() == QDialog.Accepted:
|
||||||
|
|
||||||
|
# --- START OF MODIFICATION ---
|
||||||
if hasattr(dialog, 'update_profiles_list') and dialog.update_profiles_list:
|
if hasattr(dialog, 'update_profiles_list') and dialog.update_profiles_list:
|
||||||
self.active_update_profiles_list = dialog.update_profiles_list
|
self.active_update_profiles_list = dialog.update_profiles_list
|
||||||
|
|
||||||
|
# --- NEW LOGIC: Check if user wants to load settings into UI ---
|
||||||
load_settings_requested = getattr(dialog, 'load_settings_into_ui_requested', False)
|
load_settings_requested = getattr(dialog, 'load_settings_into_ui_requested', False)
|
||||||
self.override_update_profile_settings = load_settings_requested
|
self.override_update_profile_settings = load_settings_requested
|
||||||
|
|
||||||
@ -7231,7 +7239,7 @@ class DownloaderApp (QWidget ):
|
|||||||
self.link_input.setText(f"{len(self.active_update_profiles_list)} profiles loaded for update check...")
|
self.link_input.setText(f"{len(self.active_update_profiles_list)} profiles loaded for update check...")
|
||||||
|
|
||||||
self._start_batch_update_check(self.active_update_profiles_list)
|
self._start_batch_update_check(self.active_update_profiles_list)
|
||||||
|
# --- END OF MODIFICATION ---
|
||||||
|
|
||||||
elif hasattr(dialog, 'selected_creators_for_queue') and dialog.selected_creators_for_queue:
|
elif hasattr(dialog, 'selected_creators_for_queue') and dialog.selected_creators_for_queue:
|
||||||
self.active_update_profile = None # Ensure single update mode is off
|
self.active_update_profile = None # Ensure single update mode is off
|
||||||
@ -7480,13 +7488,17 @@ class DownloaderApp (QWidget ):
|
|||||||
|
|
||||||
should_create_artist_folder = False
|
should_create_artist_folder = False
|
||||||
|
|
||||||
|
# --- Check for popup selection scope ---
|
||||||
if item_type == 'creator_popup_selection' and item_scope == EmptyPopupDialog.SCOPE_CREATORS:
|
if item_type == 'creator_popup_selection' and item_scope == EmptyPopupDialog.SCOPE_CREATORS:
|
||||||
should_create_artist_folder = True
|
should_create_artist_folder = True
|
||||||
|
# --- Check for global "Artist Folders" scope ---
|
||||||
elif item_type != 'creator_popup_selection' and self.favorite_download_scope == FAVORITE_SCOPE_ARTIST_FOLDERS:
|
elif item_type != 'creator_popup_selection' and self.favorite_download_scope == FAVORITE_SCOPE_ARTIST_FOLDERS:
|
||||||
should_create_artist_folder = True
|
should_create_artist_folder = True
|
||||||
|
|
||||||
|
# --- NEW: Check for forced folder flag from batch ---
|
||||||
if self.current_processing_favorite_item_info.get('force_artist_folder'):
|
if self.current_processing_favorite_item_info.get('force_artist_folder'):
|
||||||
should_create_artist_folder = True
|
should_create_artist_folder = True
|
||||||
|
# ---------------------------------------------------
|
||||||
|
|
||||||
if should_create_artist_folder and main_download_dir:
|
if should_create_artist_folder and main_download_dir:
|
||||||
folder_name_key = self.current_processing_favorite_item_info.get('name_for_folder', 'Unknown_Folder')
|
folder_name_key = self.current_processing_favorite_item_info.get('name_for_folder', 'Unknown_Folder')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user