mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-29 16:14:44 +00:00
commit
This commit is contained in:
@@ -120,7 +120,7 @@ def download_from_api(
|
|||||||
selected_cookie_file=None,
|
selected_cookie_file=None,
|
||||||
app_base_dir=None,
|
app_base_dir=None,
|
||||||
manga_filename_style_for_sort_check=None,
|
manga_filename_style_for_sort_check=None,
|
||||||
processed_post_ids=None # --- ADD THIS ARGUMENT ---
|
processed_post_ids=None
|
||||||
):
|
):
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': 'Mozilla/5.0',
|
'User-Agent': 'Mozilla/5.0',
|
||||||
@@ -139,9 +139,19 @@ def download_from_api(
|
|||||||
|
|
||||||
parsed_input_url_for_domain = urlparse(api_url_input)
|
parsed_input_url_for_domain = urlparse(api_url_input)
|
||||||
api_domain = parsed_input_url_for_domain.netloc
|
api_domain = parsed_input_url_for_domain.netloc
|
||||||
if not any(d in api_domain.lower() for d in ['kemono.su', 'kemono.party', 'coomer.su', 'coomer.party']):
|
fallback_api_domain = None
|
||||||
logger(f"⚠️ Unrecognized domain '{api_domain}' from input URL. Defaulting to kemono.su for API calls.")
|
|
||||||
api_domain = "kemono.su"
|
# --- START: MODIFIED DOMAIN LOGIC WITH FALLBACK ---
|
||||||
|
if 'kemono.cr' in api_domain.lower():
|
||||||
|
fallback_api_domain = 'kemono.su'
|
||||||
|
elif 'coomer.st' in api_domain.lower():
|
||||||
|
fallback_api_domain = 'coomer.su'
|
||||||
|
elif not any(d in api_domain.lower() for d in ['kemono.su', 'kemono.party', 'kemono.cr', 'coomer.su', 'coomer.party', 'coomer.st']):
|
||||||
|
logger(f"⚠️ Unrecognized domain '{api_domain}'. Defaulting to kemono.cr with fallback to kemono.su.")
|
||||||
|
api_domain = "kemono.cr"
|
||||||
|
fallback_api_domain = "kemono.su"
|
||||||
|
# --- END: MODIFIED DOMAIN LOGIC WITH FALLBACK ---
|
||||||
|
|
||||||
cookies_for_api = None
|
cookies_for_api = None
|
||||||
if use_cookie and app_base_dir:
|
if use_cookie and app_base_dir:
|
||||||
cookies_for_api = prepare_cookies_for_request(use_cookie, cookie_text, selected_cookie_file, app_base_dir, logger, target_domain=api_domain)
|
cookies_for_api = prepare_cookies_for_request(use_cookie, cookie_text, selected_cookie_file, app_base_dir, logger, target_domain=api_domain)
|
||||||
@@ -178,7 +188,6 @@ def download_from_api(
|
|||||||
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).")
|
||||||
|
|
||||||
is_manga_mode_fetch_all_and_sort_oldest_first = manga_mode and (manga_filename_style_for_sort_check != STYLE_DATE_POST_TITLE) and not target_post_id
|
is_manga_mode_fetch_all_and_sort_oldest_first = manga_mode and (manga_filename_style_for_sort_check != STYLE_DATE_POST_TITLE) and not target_post_id
|
||||||
api_base_url = f"https://{api_domain}/api/v1/{service}/user/{user_id}"
|
|
||||||
page_size = 50
|
page_size = 50
|
||||||
if is_manga_mode_fetch_all_and_sort_oldest_first:
|
if is_manga_mode_fetch_all_and_sort_oldest_first:
|
||||||
logger(f" Manga Mode (Style: {manga_filename_style_for_sort_check if manga_filename_style_for_sort_check else 'Default'} - Oldest First Sort Active): Fetching all posts to sort by date...")
|
logger(f" Manga Mode (Style: {manga_filename_style_for_sort_check if manga_filename_style_for_sort_check else 'Default'} - Oldest First Sort Active): Fetching all posts to sort by date...")
|
||||||
@@ -191,6 +200,12 @@ def download_from_api(
|
|||||||
logger(f" Manga Mode: Starting fetch from page 1 (offset 0).")
|
logger(f" Manga Mode: Starting fetch from page 1 (offset 0).")
|
||||||
if end_page:
|
if end_page:
|
||||||
logger(f" Manga Mode: Will fetch up to page {end_page}.")
|
logger(f" Manga Mode: Will fetch up to page {end_page}.")
|
||||||
|
|
||||||
|
# --- START: MANGA MODE FALLBACK LOGIC ---
|
||||||
|
is_first_page_attempt_manga = True
|
||||||
|
api_base_url_manga = f"https://{api_domain}/api/v1/{service}/user/{user_id}"
|
||||||
|
# --- END: MANGA MODE FALLBACK LOGIC ---
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if pause_event and pause_event.is_set():
|
if pause_event and pause_event.is_set():
|
||||||
logger(" Manga mode post fetching paused...")
|
logger(" Manga mode post fetching paused...")
|
||||||
@@ -208,7 +223,10 @@ def download_from_api(
|
|||||||
logger(f" Manga Mode: Reached specified end page ({end_page}). Stopping post fetch.")
|
logger(f" Manga Mode: Reached specified end page ({end_page}). Stopping post fetch.")
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
posts_batch_manga = fetch_posts_paginated(api_base_url, headers, current_offset_manga, logger, cancellation_event, pause_event, cookies_dict=cookies_for_api)
|
# --- START: MANGA MODE FALLBACK EXECUTION ---
|
||||||
|
posts_batch_manga = fetch_posts_paginated(api_base_url_manga, headers, current_offset_manga, logger, cancellation_event, pause_event, cookies_dict=cookies_for_api)
|
||||||
|
is_first_page_attempt_manga = False # Success, no need to fallback
|
||||||
|
# --- END: MANGA MODE FALLBACK EXECUTION ---
|
||||||
if not isinstance(posts_batch_manga, list):
|
if not isinstance(posts_batch_manga, list):
|
||||||
logger(f"❌ API Error (Manga Mode): Expected list of posts, got {type(posts_batch_manga)}.")
|
logger(f"❌ API Error (Manga Mode): Expected list of posts, got {type(posts_batch_manga)}.")
|
||||||
break
|
break
|
||||||
@@ -220,9 +238,21 @@ def download_from_api(
|
|||||||
logger(f" Manga Mode: No posts found within the specified page range ({start_page or 1}-{end_page}).")
|
logger(f" Manga Mode: No posts found within the specified page range ({start_page or 1}-{end_page}).")
|
||||||
break
|
break
|
||||||
all_posts_for_manga_mode.extend(posts_batch_manga)
|
all_posts_for_manga_mode.extend(posts_batch_manga)
|
||||||
|
|
||||||
|
logger(f"MANGA_FETCH_PROGRESS:{len(all_posts_for_manga_mode)}:{current_page_num_manga}")
|
||||||
|
|
||||||
current_offset_manga += page_size
|
current_offset_manga += page_size
|
||||||
time.sleep(0.6)
|
time.sleep(0.6)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
|
# --- START: MANGA MODE FALLBACK HANDLING ---
|
||||||
|
if is_first_page_attempt_manga and fallback_api_domain:
|
||||||
|
logger(f" ⚠️ Initial API fetch (Manga Mode) from '{api_domain}' failed: {e}")
|
||||||
|
logger(f" ↪️ Falling back to old domain: '{fallback_api_domain}'")
|
||||||
|
api_domain = fallback_api_domain
|
||||||
|
api_base_url_manga = f"https://{api_domain}/api/v1/{service}/user/{user_id}"
|
||||||
|
is_first_page_attempt_manga = False
|
||||||
|
continue # Retry the same offset with the new domain
|
||||||
|
# --- END: MANGA MODE FALLBACK HANDLING ---
|
||||||
if "cancelled by user" in str(e).lower():
|
if "cancelled by user" in str(e).lower():
|
||||||
logger(f"ℹ️ Manga mode pagination stopped due to cancellation: {e}")
|
logger(f"ℹ️ Manga mode pagination stopped due to cancellation: {e}")
|
||||||
else:
|
else:
|
||||||
@@ -232,7 +262,12 @@ def download_from_api(
|
|||||||
logger(f"❌ Unexpected error during manga mode fetch: {e}")
|
logger(f"❌ Unexpected error during manga mode fetch: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
break
|
break
|
||||||
|
|
||||||
if cancellation_event and cancellation_event.is_set(): return
|
if cancellation_event and cancellation_event.is_set(): return
|
||||||
|
|
||||||
|
if all_posts_for_manga_mode:
|
||||||
|
logger(f"MANGA_FETCH_COMPLETE:{len(all_posts_for_manga_mode)}")
|
||||||
|
|
||||||
if all_posts_for_manga_mode:
|
if all_posts_for_manga_mode:
|
||||||
if processed_post_ids:
|
if processed_post_ids:
|
||||||
original_count = len(all_posts_for_manga_mode)
|
original_count = len(all_posts_for_manga_mode)
|
||||||
@@ -278,6 +313,12 @@ def download_from_api(
|
|||||||
current_offset = (start_page - 1) * page_size
|
current_offset = (start_page - 1) * page_size
|
||||||
current_page_num = start_page
|
current_page_num = start_page
|
||||||
logger(f" Starting from page {current_page_num} (calculated offset {current_offset}).")
|
logger(f" Starting from page {current_page_num} (calculated offset {current_offset}).")
|
||||||
|
|
||||||
|
# --- START: STANDARD PAGINATION FALLBACK LOGIC ---
|
||||||
|
is_first_page_attempt = True
|
||||||
|
api_base_url = f"https://{api_domain}/api/v1/{service}/user/{user_id}"
|
||||||
|
# --- END: STANDARD PAGINATION FALLBACK LOGIC ---
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if pause_event and pause_event.is_set():
|
if pause_event and pause_event.is_set():
|
||||||
logger(" Post fetching loop paused...")
|
logger(" Post fetching loop paused...")
|
||||||
@@ -296,11 +337,23 @@ def download_from_api(
|
|||||||
logger(f"✅ Reached specified end page ({end_page}) for creator feed. Stopping.")
|
logger(f"✅ Reached specified end page ({end_page}) for creator feed. Stopping.")
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
|
# --- START: STANDARD PAGINATION FALLBACK EXECUTION ---
|
||||||
posts_batch = fetch_posts_paginated(api_base_url, headers, current_offset, logger, cancellation_event, pause_event, cookies_dict=cookies_for_api)
|
posts_batch = fetch_posts_paginated(api_base_url, headers, current_offset, logger, cancellation_event, pause_event, cookies_dict=cookies_for_api)
|
||||||
|
is_first_page_attempt = False # Success, no more fallbacks needed
|
||||||
|
# --- END: STANDARD PAGINATION FALLBACK EXECUTION ---
|
||||||
if not isinstance(posts_batch, list):
|
if not isinstance(posts_batch, list):
|
||||||
logger(f"❌ API Error: Expected list of posts, got {type(posts_batch)} at page {current_page_num} (offset {current_offset}).")
|
logger(f"❌ API Error: Expected list of posts, got {type(posts_batch)} at page {current_page_num} (offset {current_offset}).")
|
||||||
break
|
break
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
|
# --- START: STANDARD PAGINATION FALLBACK HANDLING ---
|
||||||
|
if is_first_page_attempt and fallback_api_domain:
|
||||||
|
logger(f" ⚠️ Initial API fetch from '{api_domain}' failed: {e}")
|
||||||
|
logger(f" ↪️ Falling back to old domain: '{fallback_api_domain}'")
|
||||||
|
api_domain = fallback_api_domain
|
||||||
|
api_base_url = f"https://{api_domain}/api/v1/{service}/user/{user_id}"
|
||||||
|
is_first_page_attempt = False
|
||||||
|
continue # Retry the same offset with the new domain
|
||||||
|
# --- END: STANDARD PAGINATION FALLBACK HANDLING ---
|
||||||
if "cancelled by user" in str(e).lower():
|
if "cancelled by user" in str(e).lower():
|
||||||
logger(f"ℹ️ Pagination stopped due to cancellation: {e}")
|
logger(f"ℹ️ Pagination stopped due to cancellation: {e}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import json
|
|||||||
import traceback
|
import traceback
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed, Future
|
from concurrent.futures import ThreadPoolExecutor, as_completed, Future
|
||||||
from .api_client import download_from_api
|
from .api_client import download_from_api
|
||||||
from .workers import PostProcessorWorker, DownloadThread
|
from .workers import PostProcessorWorker
|
||||||
from ..config.constants import (
|
from ..config.constants import (
|
||||||
STYLE_DATE_BASED, STYLE_POST_TITLE_GLOBAL_NUMBERING,
|
STYLE_DATE_BASED, STYLE_POST_TITLE_GLOBAL_NUMBERING,
|
||||||
MAX_THREADS, POST_WORKER_BATCH_THRESHOLD, POST_WORKER_NUM_BATCHES,
|
MAX_THREADS
|
||||||
POST_WORKER_BATCH_DELAY_SECONDS
|
|
||||||
)
|
)
|
||||||
from ..utils.file_utils import clean_folder_name
|
from ..utils.file_utils import clean_folder_name
|
||||||
|
|
||||||
@@ -44,6 +43,7 @@ class DownloadManager:
|
|||||||
self.creator_profiles_dir = None
|
self.creator_profiles_dir = None
|
||||||
self.current_creator_name_for_profile = None
|
self.current_creator_name_for_profile = None
|
||||||
self.current_creator_profile_path = None
|
self.current_creator_profile_path = None
|
||||||
|
self.session_file_path = None
|
||||||
|
|
||||||
def _log(self, message):
|
def _log(self, message):
|
||||||
"""Puts a progress message into the queue for the UI."""
|
"""Puts a progress message into the queue for the UI."""
|
||||||
@@ -62,7 +62,11 @@ class DownloadManager:
|
|||||||
self._log("❌ Cannot start a new session: A session is already in progress.")
|
self._log("❌ Cannot start a new session: A session is already in progress.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.session_file_path = config.get('session_file_path')
|
||||||
creator_profile_data = self._setup_creator_profile(config)
|
creator_profile_data = self._setup_creator_profile(config)
|
||||||
|
|
||||||
|
# Save settings to profile at the start of the session
|
||||||
|
if self.current_creator_profile_path:
|
||||||
creator_profile_data['settings'] = config
|
creator_profile_data['settings'] = config
|
||||||
creator_profile_data.setdefault('processed_post_ids', [])
|
creator_profile_data.setdefault('processed_post_ids', [])
|
||||||
self._save_creator_profile(creator_profile_data)
|
self._save_creator_profile(creator_profile_data)
|
||||||
@@ -77,6 +81,7 @@ class DownloadManager:
|
|||||||
self.total_downloads = 0
|
self.total_downloads = 0
|
||||||
self.total_skips = 0
|
self.total_skips = 0
|
||||||
self.all_kept_original_filenames = []
|
self.all_kept_original_filenames = []
|
||||||
|
|
||||||
is_single_post = bool(config.get('target_post_id_from_initial_url'))
|
is_single_post = bool(config.get('target_post_id_from_initial_url'))
|
||||||
use_multithreading = config.get('use_multithreading', True)
|
use_multithreading = config.get('use_multithreading', True)
|
||||||
is_manga_sequential = config.get('manga_mode_active') and config.get('manga_filename_style') in [STYLE_DATE_BASED, STYLE_POST_TITLE_GLOBAL_NUMBERING]
|
is_manga_sequential = config.get('manga_mode_active') and config.get('manga_filename_style') in [STYLE_DATE_BASED, STYLE_POST_TITLE_GLOBAL_NUMBERING]
|
||||||
@@ -86,88 +91,54 @@ class DownloadManager:
|
|||||||
if should_use_multithreading_for_posts:
|
if should_use_multithreading_for_posts:
|
||||||
fetcher_thread = threading.Thread(
|
fetcher_thread = threading.Thread(
|
||||||
target=self._fetch_and_queue_posts_for_pool,
|
target=self._fetch_and_queue_posts_for_pool,
|
||||||
args=(config, restore_data, creator_profile_data), # Add argument here
|
args=(config, restore_data, creator_profile_data),
|
||||||
daemon=True
|
daemon=True
|
||||||
)
|
)
|
||||||
fetcher_thread.start()
|
fetcher_thread.start()
|
||||||
else:
|
else:
|
||||||
self._start_single_threaded_session(config)
|
# Single-threaded mode does not use the manager's complex logic
|
||||||
|
self._log("ℹ️ Manager is handing off to a single-threaded worker...")
|
||||||
|
# The single-threaded worker will manage its own lifecycle and signals.
|
||||||
|
# The manager's role for this session is effectively over.
|
||||||
|
self.is_running = False # Allow another session to start if needed
|
||||||
|
self.progress_queue.put({'type': 'handoff_to_single_thread', 'payload': (config,)})
|
||||||
|
|
||||||
def _start_single_threaded_session(self, config):
|
|
||||||
"""Handles downloads that are best processed by a single worker thread."""
|
|
||||||
self._log("ℹ️ Initializing single-threaded download process...")
|
|
||||||
self.worker_thread = threading.Thread(
|
|
||||||
target=self._run_single_worker,
|
|
||||||
args=(config,),
|
|
||||||
daemon=True
|
|
||||||
)
|
|
||||||
self.worker_thread.start()
|
|
||||||
|
|
||||||
def _run_single_worker(self, config):
|
def _fetch_and_queue_posts_for_pool(self, config, restore_data, creator_profile_data):
|
||||||
"""Target function for the single-worker thread."""
|
|
||||||
try:
|
|
||||||
worker = DownloadThread(config, self.progress_queue)
|
|
||||||
worker.run() # This is the main blocking call for this thread
|
|
||||||
except Exception as e:
|
|
||||||
self._log(f"❌ CRITICAL ERROR in single-worker thread: {e}")
|
|
||||||
self._log(traceback.format_exc())
|
|
||||||
finally:
|
|
||||||
self.is_running = False
|
|
||||||
|
|
||||||
def _fetch_and_queue_posts_for_pool(self, config, restore_data):
|
|
||||||
"""
|
"""
|
||||||
Fetches all posts from the API and submits them as tasks to a thread pool.
|
Fetches posts from the API in batches and submits them as tasks to a thread pool.
|
||||||
This method runs in its own dedicated thread to avoid blocking.
|
This method runs in its own dedicated thread to avoid blocking the UI.
|
||||||
|
It provides immediate feedback as soon as the first batch of posts is found.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
num_workers = min(config.get('num_threads', 4), MAX_THREADS)
|
num_workers = min(config.get('num_threads', 4), MAX_THREADS)
|
||||||
self.thread_pool = ThreadPoolExecutor(max_workers=num_workers, thread_name_prefix='PostWorker_')
|
self.thread_pool = ThreadPoolExecutor(max_workers=num_workers, thread_name_prefix='PostWorker_')
|
||||||
|
|
||||||
session_processed_ids = set(restore_data['processed_post_ids']) if restore_data else set()
|
session_processed_ids = set(restore_data.get('processed_post_ids', [])) if restore_data else set()
|
||||||
profile_processed_ids = set(creator_profile_data.get('processed_post_ids', []))
|
profile_processed_ids = set(creator_profile_data.get('processed_post_ids', []))
|
||||||
processed_ids = session_processed_ids.union(profile_processed_ids)
|
processed_ids = session_processed_ids.union(profile_processed_ids)
|
||||||
|
|
||||||
if restore_data:
|
if restore_data and 'all_posts_data' in restore_data:
|
||||||
|
# This logic for session restore remains as it relies on a pre-fetched list
|
||||||
all_posts = restore_data['all_posts_data']
|
all_posts = restore_data['all_posts_data']
|
||||||
processed_ids = set(restore_data['processed_post_ids'])
|
|
||||||
posts_to_process = [p for p in all_posts if p.get('id') not in processed_ids]
|
posts_to_process = [p for p in all_posts if p.get('id') not in processed_ids]
|
||||||
self.total_posts = len(all_posts)
|
self.total_posts = len(all_posts)
|
||||||
self.processed_posts = len(processed_ids)
|
self.processed_posts = len(processed_ids)
|
||||||
self._log(f"🔄 Restoring session. {len(posts_to_process)} posts remaining.")
|
self._log(f"🔄 Restoring session. {len(posts_to_process)} posts remaining.")
|
||||||
else:
|
|
||||||
posts_to_process = self._get_all_posts(config)
|
|
||||||
self.total_posts = len(posts_to_process)
|
|
||||||
self.processed_posts = 0
|
|
||||||
|
|
||||||
self.progress_queue.put({'type': 'overall_progress', 'payload': (self.total_posts, self.processed_posts)})
|
self.progress_queue.put({'type': 'overall_progress', 'payload': (self.total_posts, self.processed_posts)})
|
||||||
|
|
||||||
if not posts_to_process:
|
if not posts_to_process:
|
||||||
self._log("✅ No new posts to process.")
|
self._log("✅ No new posts to process from restored session.")
|
||||||
return
|
return
|
||||||
|
|
||||||
for post_data in posts_to_process:
|
for post_data in posts_to_process:
|
||||||
if self.cancellation_event.is_set():
|
if self.cancellation_event.is_set(): break
|
||||||
break
|
|
||||||
worker = PostProcessorWorker(post_data, config, self.progress_queue)
|
worker = PostProcessorWorker(post_data, config, self.progress_queue)
|
||||||
future = self.thread_pool.submit(worker.process)
|
future = self.thread_pool.submit(worker.process)
|
||||||
future.add_done_callback(self._handle_future_result)
|
future.add_done_callback(self._handle_future_result)
|
||||||
self.active_futures.append(future)
|
self.active_futures.append(future)
|
||||||
|
else:
|
||||||
except Exception as e:
|
# --- START: REFACTORED STREAMING LOGIC ---
|
||||||
self._log(f"❌ CRITICAL ERROR in post fetcher thread: {e}")
|
|
||||||
self._log(traceback.format_exc())
|
|
||||||
finally:
|
|
||||||
if self.thread_pool:
|
|
||||||
self.thread_pool.shutdown(wait=True)
|
|
||||||
self.is_running = False
|
|
||||||
self._log("🏁 All processing tasks have completed or been cancelled.")
|
|
||||||
self.progress_queue.put({
|
|
||||||
'type': 'finished',
|
|
||||||
'payload': (self.total_downloads, self.total_skips, self.cancellation_event.is_set(), self.all_kept_original_filenames)
|
|
||||||
})
|
|
||||||
|
|
||||||
def _get_all_posts(self, config):
|
|
||||||
"""Helper to fetch all posts using the API client."""
|
|
||||||
all_posts = []
|
|
||||||
post_generator = download_from_api(
|
post_generator = download_from_api(
|
||||||
api_url_input=config['api_url'],
|
api_url_input=config['api_url'],
|
||||||
logger=self._log,
|
logger=self._log,
|
||||||
@@ -181,11 +152,50 @@ class DownloadManager:
|
|||||||
selected_cookie_file=config.get('selected_cookie_file'),
|
selected_cookie_file=config.get('selected_cookie_file'),
|
||||||
app_base_dir=config.get('app_base_dir'),
|
app_base_dir=config.get('app_base_dir'),
|
||||||
manga_filename_style_for_sort_check=config.get('manga_filename_style'),
|
manga_filename_style_for_sort_check=config.get('manga_filename_style'),
|
||||||
processed_post_ids=config.get('processed_post_ids', [])
|
processed_post_ids=list(processed_ids)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.total_posts = 0
|
||||||
|
self.processed_posts = 0
|
||||||
|
|
||||||
|
# Process posts in batches as they are yielded by the API client
|
||||||
for batch in post_generator:
|
for batch in post_generator:
|
||||||
all_posts.extend(batch)
|
if self.cancellation_event.is_set():
|
||||||
return all_posts
|
self._log(" Post fetching cancelled.")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Filter out any posts that might have been processed since the start
|
||||||
|
posts_in_batch_to_process = [p for p in batch if p.get('id') not in processed_ids]
|
||||||
|
|
||||||
|
if not posts_in_batch_to_process:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update total count and immediately inform the UI
|
||||||
|
self.total_posts += len(posts_in_batch_to_process)
|
||||||
|
self.progress_queue.put({'type': 'overall_progress', 'payload': (self.total_posts, self.processed_posts)})
|
||||||
|
|
||||||
|
for post_data in posts_in_batch_to_process:
|
||||||
|
if self.cancellation_event.is_set(): break
|
||||||
|
worker = PostProcessorWorker(post_data, config, self.progress_queue)
|
||||||
|
future = self.thread_pool.submit(worker.process)
|
||||||
|
future.add_done_callback(self._handle_future_result)
|
||||||
|
self.active_futures.append(future)
|
||||||
|
|
||||||
|
if self.total_posts == 0 and not self.cancellation_event.is_set():
|
||||||
|
self._log("✅ No new posts found to process.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ CRITICAL ERROR in post fetcher thread: {e}")
|
||||||
|
self._log(traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
if self.thread_pool:
|
||||||
|
self.thread_pool.shutdown(wait=True)
|
||||||
|
self.is_running = False
|
||||||
|
self._log("🏁 All processing tasks have completed or been cancelled.")
|
||||||
|
self.progress_queue.put({
|
||||||
|
'type': 'finished',
|
||||||
|
'payload': (self.total_downloads, self.total_skips, self.cancellation_event.is_set(), self.all_kept_original_filenames)
|
||||||
|
})
|
||||||
|
|
||||||
def _handle_future_result(self, future: Future):
|
def _handle_future_result(self, future: Future):
|
||||||
"""Callback executed when a worker task completes."""
|
"""Callback executed when a worker task completes."""
|
||||||
@@ -261,9 +271,15 @@ class DownloadManager:
|
|||||||
"""Cancels the current running session."""
|
"""Cancels the current running session."""
|
||||||
if not self.is_running:
|
if not self.is_running:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.cancellation_event.is_set():
|
||||||
|
self._log("ℹ️ Cancellation already in progress.")
|
||||||
|
return
|
||||||
|
|
||||||
self._log("⚠️ Cancellation requested by user...")
|
self._log("⚠️ Cancellation requested by user...")
|
||||||
self.cancellation_event.set()
|
self.cancellation_event.set()
|
||||||
if self.thread_pool:
|
|
||||||
self.thread_pool.shutdown(wait=False, cancel_futures=True)
|
|
||||||
|
|
||||||
self.is_running = False
|
if self.thread_pool:
|
||||||
|
self._log(" Signaling all worker threads to stop and shutting down pool...")
|
||||||
|
self.thread_pool.shutdown(wait=False)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import queue
|
import queue
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
@@ -1175,11 +1176,18 @@ class PostProcessorWorker:
|
|||||||
if FPDF:
|
if FPDF:
|
||||||
self.logger(f" Creating formatted PDF for {'comments' if self.text_only_scope == 'comments' else 'content'}...")
|
self.logger(f" Creating formatted PDF for {'comments' if self.text_only_scope == 'comments' else 'content'}...")
|
||||||
pdf = PDF()
|
pdf = PDF()
|
||||||
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
|
# If the application is run as a bundled exe, _MEIPASS is the temp folder
|
||||||
|
base_path = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
# If running as a normal .py script, use the project_root_dir
|
||||||
|
base_path = self.project_root_dir
|
||||||
|
|
||||||
font_path = ""
|
font_path = ""
|
||||||
bold_font_path = ""
|
bold_font_path = ""
|
||||||
if self.project_root_dir:
|
if base_path:
|
||||||
font_path = os.path.join(self.project_root_dir, 'data', 'dejavu-sans', 'DejaVuSans.ttf')
|
font_path = os.path.join(base_path, 'data', 'dejavu-sans', 'DejaVuSans.ttf')
|
||||||
bold_font_path = os.path.join(self.project_root_dir, 'data', 'dejavu-sans', 'DejaVuSans-Bold.ttf')
|
bold_font_path = os.path.join(base_path, 'data', 'dejavu-sans', 'DejaVuSans-Bold.ttf')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(font_path): raise RuntimeError(f"Font file not found: {font_path}")
|
if not os.path.exists(font_path): raise RuntimeError(f"Font file not found: {font_path}")
|
||||||
@@ -1666,10 +1674,12 @@ class PostProcessorWorker:
|
|||||||
if not self.extract_links_only and self.use_post_subfolders and total_downloaded_this_post == 0:
|
if not self.extract_links_only and self.use_post_subfolders and total_downloaded_this_post == 0:
|
||||||
path_to_check_for_emptiness = determined_post_save_path_for_history
|
path_to_check_for_emptiness = determined_post_save_path_for_history
|
||||||
try:
|
try:
|
||||||
|
# Check if the path is a directory and if it's empty
|
||||||
if os.path.isdir(path_to_check_for_emptiness) and not os.listdir(path_to_check_for_emptiness):
|
if os.path.isdir(path_to_check_for_emptiness) and not os.listdir(path_to_check_for_emptiness):
|
||||||
self.logger(f" 🗑️ Removing empty post-specific subfolder: '{path_to_check_for_emptiness}'")
|
self.logger(f" 🗑️ Removing empty post-specific subfolder: '{path_to_check_for_emptiness}'")
|
||||||
os.rmdir(path_to_check_for_emptiness)
|
os.rmdir(path_to_check_for_emptiness)
|
||||||
except OSError as e_rmdir:
|
except OSError as e_rmdir:
|
||||||
|
# Log if removal fails for any reason (e.g., permissions)
|
||||||
self.logger(f" ⚠️ Could not remove empty post-specific subfolder '{path_to_check_for_emptiness}': {e_rmdir}")
|
self.logger(f" ⚠️ Could not remove empty post-specific subfolder '{path_to_check_for_emptiness}': {e_rmdir}")
|
||||||
|
|
||||||
result_tuple = (total_downloaded_this_post, total_skipped_this_post,
|
result_tuple = (total_downloaded_this_post, total_skipped_this_post,
|
||||||
@@ -1678,6 +1688,15 @@ class PostProcessorWorker:
|
|||||||
None)
|
None)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
if not self.extract_links_only and self.use_post_subfolders and total_downloaded_this_post == 0:
|
||||||
|
path_to_check_for_emptiness = determined_post_save_path_for_history
|
||||||
|
try:
|
||||||
|
if os.path.isdir(path_to_check_for_emptiness) and not os.listdir(path_to_check_for_emptiness):
|
||||||
|
self.logger(f" 🗑️ Removing empty post-specific subfolder: '{path_to_check_for_emptiness}'")
|
||||||
|
os.rmdir(path_to_check_for_emptiness)
|
||||||
|
except OSError as e_rmdir:
|
||||||
|
self.logger(f" ⚠️ Could not remove potentially empty subfolder '{path_to_check_for_emptiness}': {e_rmdir}")
|
||||||
|
|
||||||
self._emit_signal('worker_finished', result_tuple)
|
self._emit_signal('worker_finished', result_tuple)
|
||||||
|
|
||||||
return result_tuple
|
return result_tuple
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ MAX_CHUNK_DOWNLOAD_RETRIES = 1
|
|||||||
DOWNLOAD_CHUNK_SIZE_ITER = 1024 * 256 # 256 KB per iteration chunk
|
DOWNLOAD_CHUNK_SIZE_ITER = 1024 * 256 # 256 KB per iteration chunk
|
||||||
|
|
||||||
# Flag to indicate if this module and its dependencies are available.
|
# Flag to indicate if this module and its dependencies are available.
|
||||||
# This was missing and caused the ImportError.
|
|
||||||
MULTIPART_DOWNLOADER_AVAILABLE = True
|
MULTIPART_DOWNLOADER_AVAILABLE = True
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +48,13 @@ def _download_individual_chunk(
|
|||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Download resumed.")
|
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Download resumed.")
|
||||||
|
|
||||||
|
# --- START: FIX ---
|
||||||
|
# Set this chunk's status to 'active' before starting the download.
|
||||||
|
with progress_data['lock']:
|
||||||
|
progress_data['chunks_status'][part_num]['active'] = True
|
||||||
|
# --- END: FIX ---
|
||||||
|
|
||||||
|
try:
|
||||||
# Prepare headers for the specific byte range of this chunk
|
# Prepare headers for the specific byte range of this chunk
|
||||||
chunk_headers = headers.copy()
|
chunk_headers = headers.copy()
|
||||||
if end_byte != -1:
|
if end_byte != -1:
|
||||||
@@ -117,7 +123,6 @@ def _download_individual_chunk(
|
|||||||
elif hasattr(emitter, 'file_progress_signal'):
|
elif hasattr(emitter, 'file_progress_signal'):
|
||||||
emitter.file_progress_signal.emit(api_original_filename, status_list_copy)
|
emitter.file_progress_signal.emit(api_original_filename, status_list_copy)
|
||||||
|
|
||||||
# If we reach here, the download for this chunk was successful
|
|
||||||
return bytes_this_chunk, True
|
return bytes_this_chunk, True
|
||||||
|
|
||||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout, http.client.IncompleteRead) as e:
|
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout, http.client.IncompleteRead) as e:
|
||||||
@@ -130,6 +135,10 @@ def _download_individual_chunk(
|
|||||||
return bytes_this_chunk, False
|
return bytes_this_chunk, False
|
||||||
|
|
||||||
return bytes_this_chunk, False
|
return bytes_this_chunk, False
|
||||||
|
finally:
|
||||||
|
with progress_data['lock']:
|
||||||
|
progress_data['chunks_status'][part_num]['active'] = False
|
||||||
|
progress_data['chunks_status'][part_num]['speed_bps'] = 0.0
|
||||||
|
|
||||||
|
|
||||||
def download_file_in_parts(file_url, save_path, total_size, num_parts, headers, api_original_filename,
|
def download_file_in_parts(file_url, save_path, total_size, num_parts, headers, api_original_filename,
|
||||||
|
|||||||
@@ -960,15 +960,16 @@ class EmptyPopupDialog (QDialog ):
|
|||||||
|
|
||||||
self .parent_app .log_signal .emit (f"ℹ️ Added {num_just_added_posts } selected posts to the download queue. Total in queue: {total_in_queue }.")
|
self .parent_app .log_signal .emit (f"ℹ️ Added {num_just_added_posts } selected posts to the download queue. Total in queue: {total_in_queue }.")
|
||||||
|
|
||||||
|
# --- START: MODIFIED LOGIC ---
|
||||||
|
# Removed the blockSignals(True/False) calls to allow the main window's UI to update correctly.
|
||||||
if self .parent_app .link_input :
|
if self .parent_app .link_input :
|
||||||
self .parent_app .link_input .blockSignals (True )
|
|
||||||
self .parent_app .link_input .setText (
|
self .parent_app .link_input .setText (
|
||||||
self .parent_app ._tr ("popup_posts_selected_text","Posts - {count} selected").format (count =num_just_added_posts )
|
self .parent_app ._tr ("popup_posts_selected_text","Posts - {count} selected").format (count =num_just_added_posts )
|
||||||
)
|
)
|
||||||
self .parent_app .link_input .blockSignals (False )
|
|
||||||
self .parent_app .link_input .setPlaceholderText (
|
self .parent_app .link_input .setPlaceholderText (
|
||||||
self .parent_app ._tr ("items_in_queue_placeholder","{count} items in queue from popup.").format (count =total_in_queue )
|
self .parent_app ._tr ("items_in_queue_placeholder","{count} items in queue from popup.").format (count =total_in_queue )
|
||||||
)
|
)
|
||||||
|
# --- END: MODIFIED LOGIC ---
|
||||||
|
|
||||||
self.selected_creators_for_queue.clear()
|
self.selected_creators_for_queue.clear()
|
||||||
|
|
||||||
@@ -989,15 +990,12 @@ class EmptyPopupDialog (QDialog ):
|
|||||||
self .add_selected_button .setEnabled (True )
|
self .add_selected_button .setEnabled (True )
|
||||||
self .setWindowTitle (self ._tr ("creator_popup_title","Creator Selection"))
|
self .setWindowTitle (self ._tr ("creator_popup_title","Creator Selection"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _get_domain_for_service (self ,service_name ):
|
def _get_domain_for_service (self ,service_name ):
|
||||||
"""Determines the base domain for a given service."""
|
"""Determines the base domain for a given service."""
|
||||||
service_lower =service_name .lower ()
|
service_lower =service_name .lower ()
|
||||||
if service_lower in ['onlyfans','fansly']:
|
if service_lower in ['onlyfans','fansly']:
|
||||||
return "coomer.su"
|
return "coomer.st"
|
||||||
return "kemono.su"
|
return "kemono.cr"
|
||||||
|
|
||||||
def _handle_add_selected (self ):
|
def _handle_add_selected (self ):
|
||||||
"""Gathers globally selected creators and processes them."""
|
"""Gathers globally selected creators and processes them."""
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ from ...utils.resolution import get_dark_theme
|
|||||||
from ..main_window import get_app_icon_object
|
from ..main_window import get_app_icon_object
|
||||||
from ...config.constants import (
|
from ...config.constants import (
|
||||||
THEME_KEY, LANGUAGE_KEY, DOWNLOAD_LOCATION_KEY,
|
THEME_KEY, LANGUAGE_KEY, DOWNLOAD_LOCATION_KEY,
|
||||||
RESOLUTION_KEY, UI_SCALE_KEY, SAVE_CREATOR_JSON_KEY
|
RESOLUTION_KEY, UI_SCALE_KEY, SAVE_CREATOR_JSON_KEY,
|
||||||
|
COOKIE_TEXT_KEY, USE_COOKIE_KEY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -89,7 +90,9 @@ class FutureSettingsDialog(QDialog):
|
|||||||
# Default Path
|
# Default Path
|
||||||
self.default_path_label = QLabel()
|
self.default_path_label = QLabel()
|
||||||
self.save_path_button = QPushButton()
|
self.save_path_button = QPushButton()
|
||||||
self.save_path_button.clicked.connect(self._save_download_path)
|
# --- START: MODIFIED LOGIC ---
|
||||||
|
self.save_path_button.clicked.connect(self._save_cookie_and_path)
|
||||||
|
# --- END: MODIFIED LOGIC ---
|
||||||
download_window_layout.addWidget(self.default_path_label, 1, 0)
|
download_window_layout.addWidget(self.default_path_label, 1, 0)
|
||||||
download_window_layout.addWidget(self.save_path_button, 1, 1)
|
download_window_layout.addWidget(self.save_path_button, 1, 1)
|
||||||
|
|
||||||
@@ -143,11 +146,13 @@ class FutureSettingsDialog(QDialog):
|
|||||||
self.default_path_label.setText(self._tr("default_path_label", "Default Path:"))
|
self.default_path_label.setText(self._tr("default_path_label", "Default Path:"))
|
||||||
self.save_creator_json_checkbox.setText(self._tr("save_creator_json_label", "Save Creator.json file"))
|
self.save_creator_json_checkbox.setText(self._tr("save_creator_json_label", "Save Creator.json file"))
|
||||||
|
|
||||||
|
# --- START: MODIFIED LOGIC ---
|
||||||
# Buttons and Controls
|
# Buttons and Controls
|
||||||
self._update_theme_toggle_button_text()
|
self._update_theme_toggle_button_text()
|
||||||
self.save_path_button.setText(self._tr("settings_save_path_button", "Save Current Download Path"))
|
self.save_path_button.setText(self._tr("settings_save_cookie_path_button", "Save Cookie + Download Path"))
|
||||||
self.save_path_button.setToolTip(self._tr("settings_save_path_tooltip", "Save the current 'Download Location' for future sessions."))
|
self.save_path_button.setToolTip(self._tr("settings_save_cookie_path_tooltip", "Save the current 'Download Location' and Cookie settings for future sessions."))
|
||||||
self.ok_button.setText(self._tr("ok_button", "OK"))
|
self.ok_button.setText(self._tr("ok_button", "OK"))
|
||||||
|
# --- END: MODIFIED LOGIC ---
|
||||||
|
|
||||||
# Populate dropdowns
|
# Populate dropdowns
|
||||||
self._populate_display_combo_boxes()
|
self._populate_display_combo_boxes()
|
||||||
@@ -275,22 +280,43 @@ class FutureSettingsDialog(QDialog):
|
|||||||
if msg_box.clickedButton() == restart_button:
|
if msg_box.clickedButton() == restart_button:
|
||||||
self.parent_app._request_restart_application()
|
self.parent_app._request_restart_application()
|
||||||
|
|
||||||
def _save_download_path(self):
|
def _save_cookie_and_path(self):
|
||||||
|
"""Saves the current download path and/or cookie settings from the main window."""
|
||||||
|
path_saved = False
|
||||||
|
cookie_saved = False
|
||||||
|
|
||||||
|
# --- Save Download Path Logic ---
|
||||||
if hasattr(self.parent_app, 'dir_input') and self.parent_app.dir_input:
|
if hasattr(self.parent_app, 'dir_input') and self.parent_app.dir_input:
|
||||||
current_path = self.parent_app.dir_input.text().strip()
|
current_path = self.parent_app.dir_input.text().strip()
|
||||||
if current_path and os.path.isdir(current_path):
|
if current_path and os.path.isdir(current_path):
|
||||||
self.parent_app.settings.setValue(DOWNLOAD_LOCATION_KEY, current_path)
|
self.parent_app.settings.setValue(DOWNLOAD_LOCATION_KEY, current_path)
|
||||||
|
path_saved = True
|
||||||
|
|
||||||
|
# --- Save Cookie Logic ---
|
||||||
|
if hasattr(self.parent_app, 'use_cookie_checkbox'):
|
||||||
|
use_cookie = self.parent_app.use_cookie_checkbox.isChecked()
|
||||||
|
cookie_content = self.parent_app.cookie_text_input.text().strip()
|
||||||
|
|
||||||
|
if use_cookie and cookie_content:
|
||||||
|
self.parent_app.settings.setValue(USE_COOKIE_KEY, True)
|
||||||
|
self.parent_app.settings.setValue(COOKIE_TEXT_KEY, cookie_content)
|
||||||
|
cookie_saved = True
|
||||||
|
else: # Also save the 'off' state
|
||||||
|
self.parent_app.settings.setValue(USE_COOKIE_KEY, False)
|
||||||
|
self.parent_app.settings.setValue(COOKIE_TEXT_KEY, "")
|
||||||
|
|
||||||
self.parent_app.settings.sync()
|
self.parent_app.settings.sync()
|
||||||
QMessageBox.information(self,
|
|
||||||
self._tr("settings_save_path_success_title", "Path Saved"),
|
# --- User Feedback ---
|
||||||
self._tr("settings_save_path_success_message", "Download location '{path}' saved.").format(path=current_path))
|
if path_saved and cookie_saved:
|
||||||
elif not current_path:
|
message = self._tr("settings_save_both_success", "Download location and cookie settings saved.")
|
||||||
QMessageBox.warning(self,
|
elif path_saved:
|
||||||
self._tr("settings_save_path_empty_title", "Empty Path"),
|
message = self._tr("settings_save_path_only_success", "Download location saved. No cookie settings were active to save.")
|
||||||
self._tr("settings_save_path_empty_message", "Download location cannot be empty."))
|
elif cookie_saved:
|
||||||
|
message = self._tr("settings_save_cookie_only_success", "Cookie settings saved. Download location was not set.")
|
||||||
else:
|
else:
|
||||||
QMessageBox.warning(self,
|
QMessageBox.warning(self, self._tr("settings_save_nothing_title", "Nothing to Save"),
|
||||||
self._tr("settings_save_path_invalid_title", "Invalid Path"),
|
self._tr("settings_save_nothing_message", "The download location is not a valid directory and no cookie was active."))
|
||||||
self._tr("settings_save_path_invalid_message", "The path '{path}' is not a valid directory.").format(path=current_path))
|
return
|
||||||
else:
|
|
||||||
QMessageBox.critical(self, "Error", "Could not access download path input from main application.")
|
QMessageBox.information(self, self._tr("settings_save_success_title", "Settings Saved"), message)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class MoreOptionsDialog(QDialog):
|
|||||||
layout.addWidget(self.description_label)
|
layout.addWidget(self.description_label)
|
||||||
self.radio_button_group = QButtonGroup(self)
|
self.radio_button_group = QButtonGroup(self)
|
||||||
self.radio_content = QRadioButton("Description/Content")
|
self.radio_content = QRadioButton("Description/Content")
|
||||||
self.radio_comments = QRadioButton("Comments (Not Working)")
|
self.radio_comments = QRadioButton("Comments")
|
||||||
self.radio_button_group.addButton(self.radio_content)
|
self.radio_button_group.addButton(self.radio_content)
|
||||||
self.radio_button_group.addButton(self.radio_comments)
|
self.radio_button_group.addButton(self.radio_comments)
|
||||||
layout.addWidget(self.radio_content)
|
layout.addWidget(self.radio_content)
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ class DownloaderApp (QWidget ):
|
|||||||
self.active_update_profile = None
|
self.active_update_profile = None
|
||||||
self.new_posts_for_update = []
|
self.new_posts_for_update = []
|
||||||
self.is_finishing = False
|
self.is_finishing = False
|
||||||
|
self.finish_lock = threading.Lock()
|
||||||
|
|
||||||
saved_res = self.settings.value(RESOLUTION_KEY, "Auto")
|
saved_res = self.settings.value(RESOLUTION_KEY, "Auto")
|
||||||
if saved_res != "Auto":
|
if saved_res != "Auto":
|
||||||
@@ -266,7 +267,7 @@ class DownloaderApp (QWidget ):
|
|||||||
self.download_location_label_widget = None
|
self.download_location_label_widget = None
|
||||||
self.remove_from_filename_label_widget = None
|
self.remove_from_filename_label_widget = None
|
||||||
self.skip_words_label_widget = None
|
self.skip_words_label_widget = None
|
||||||
self.setWindowTitle("Kemono Downloader v6.2.0")
|
self.setWindowTitle("Kemono Downloader v6.2.1")
|
||||||
setup_ui(self)
|
setup_ui(self)
|
||||||
self._connect_signals()
|
self._connect_signals()
|
||||||
self.log_signal.emit("ℹ️ Local API server functionality has been removed.")
|
self.log_signal.emit("ℹ️ Local API server functionality has been removed.")
|
||||||
@@ -284,6 +285,7 @@ class DownloaderApp (QWidget ):
|
|||||||
self._retranslate_main_ui()
|
self._retranslate_main_ui()
|
||||||
self._load_persistent_history()
|
self._load_persistent_history()
|
||||||
self._load_saved_download_location()
|
self._load_saved_download_location()
|
||||||
|
self._load_saved_cookie_settings()
|
||||||
self._update_button_states_and_connections()
|
self._update_button_states_and_connections()
|
||||||
self._check_for_interrupted_session()
|
self._check_for_interrupted_session()
|
||||||
|
|
||||||
@@ -1570,6 +1572,31 @@ class DownloaderApp (QWidget ):
|
|||||||
QMessageBox .critical (self ,"Dialog Error",f"An unexpected error occurred with the folder selection dialog: {e }")
|
QMessageBox .critical (self ,"Dialog Error",f"An unexpected error occurred with the folder selection dialog: {e }")
|
||||||
|
|
||||||
def handle_main_log(self, message):
|
def handle_main_log(self, message):
|
||||||
|
if isinstance(message, str) and message.startswith("MANGA_FETCH_PROGRESS:"):
|
||||||
|
try:
|
||||||
|
parts = message.split(":")
|
||||||
|
fetched_count = int(parts[1])
|
||||||
|
page_num = int(parts[2])
|
||||||
|
self.progress_label.setText(self._tr("progress_fetching_manga_pages", "Progress: Fetching Page {page} ({count} posts found)...").format(page=page_num, count=fetched_count))
|
||||||
|
QCoreApplication.processEvents()
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
try:
|
||||||
|
fetched_count = int(message.split(":")[1])
|
||||||
|
self.progress_label.setText(self._tr("progress_fetching_manga_posts", "Progress: Fetching Manga Posts ({count})...").format(count=fetched_count))
|
||||||
|
QCoreApplication.processEvents()
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
elif isinstance(message, str) and message.startswith("MANGA_FETCH_COMPLETE:"):
|
||||||
|
try:
|
||||||
|
total_posts = int(message.split(":")[1])
|
||||||
|
self.total_posts_to_process = total_posts
|
||||||
|
self.processed_posts_count = 0
|
||||||
|
self.update_progress_display(self.total_posts_to_process, self.processed_posts_count)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
if message.startswith("TEMP_FILE_PATH:"):
|
if message.startswith("TEMP_FILE_PATH:"):
|
||||||
filepath = message.split(":", 1)[1]
|
filepath = message.split(":", 1)[1]
|
||||||
if self.single_pdf_setting:
|
if self.single_pdf_setting:
|
||||||
@@ -2561,8 +2588,27 @@ class DownloaderApp (QWidget ):
|
|||||||
self .manga_rename_toggle_button .setToolTip ("Click to cycle Manga Filename Style (when Manga Mode is active for a creator feed).")
|
self .manga_rename_toggle_button .setToolTip ("Click to cycle Manga Filename Style (when Manga Mode is active for a creator feed).")
|
||||||
|
|
||||||
def _toggle_manga_filename_style (self ):
|
def _toggle_manga_filename_style (self ):
|
||||||
|
url_text = self.link_input.text().strip() if self.link_input else ""
|
||||||
|
_, _, post_id = extract_post_info(url_text)
|
||||||
|
is_single_post = bool(post_id)
|
||||||
|
|
||||||
current_style = self.manga_filename_style
|
current_style = self.manga_filename_style
|
||||||
new_style = ""
|
new_style = ""
|
||||||
|
|
||||||
|
if is_single_post:
|
||||||
|
# Cycle through a limited set of styles suitable for single posts
|
||||||
|
if current_style == STYLE_POST_TITLE:
|
||||||
|
new_style = STYLE_DATE_POST_TITLE
|
||||||
|
elif current_style == STYLE_DATE_POST_TITLE:
|
||||||
|
new_style = STYLE_ORIGINAL_NAME
|
||||||
|
elif current_style == STYLE_ORIGINAL_NAME:
|
||||||
|
new_style = STYLE_POST_ID
|
||||||
|
elif current_style == STYLE_POST_ID:
|
||||||
|
new_style = STYLE_POST_TITLE
|
||||||
|
else: # Fallback for any other style
|
||||||
|
new_style = STYLE_POST_TITLE
|
||||||
|
else:
|
||||||
|
# Original cycling logic for creator feeds
|
||||||
if current_style ==STYLE_POST_TITLE :
|
if current_style ==STYLE_POST_TITLE :
|
||||||
new_style =STYLE_ORIGINAL_NAME
|
new_style =STYLE_ORIGINAL_NAME
|
||||||
elif current_style ==STYLE_ORIGINAL_NAME :
|
elif current_style ==STYLE_ORIGINAL_NAME :
|
||||||
@@ -2572,8 +2618,8 @@ class DownloaderApp (QWidget ):
|
|||||||
elif current_style ==STYLE_POST_TITLE_GLOBAL_NUMBERING :
|
elif current_style ==STYLE_POST_TITLE_GLOBAL_NUMBERING :
|
||||||
new_style =STYLE_DATE_BASED
|
new_style =STYLE_DATE_BASED
|
||||||
elif current_style ==STYLE_DATE_BASED :
|
elif current_style ==STYLE_DATE_BASED :
|
||||||
new_style =STYLE_POST_ID # Change this line
|
new_style =STYLE_POST_ID
|
||||||
elif current_style ==STYLE_POST_ID: # Add this block
|
elif current_style ==STYLE_POST_ID:
|
||||||
new_style =STYLE_POST_TITLE
|
new_style =STYLE_POST_TITLE
|
||||||
else :
|
else :
|
||||||
self .log_signal .emit (f"⚠️ Unknown current manga filename style: {current_style }. Resetting to default ('{STYLE_POST_TITLE }').")
|
self .log_signal .emit (f"⚠️ Unknown current manga filename style: {current_style }. Resetting to default ('{STYLE_POST_TITLE }').")
|
||||||
@@ -2643,16 +2689,32 @@ class DownloaderApp (QWidget ):
|
|||||||
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 )
|
||||||
|
|
||||||
|
# --- START: MODIFIED LOGIC ---
|
||||||
is_creator_feed =not post_id if url_text else False
|
is_creator_feed =not post_id if url_text else False
|
||||||
|
is_single_post = bool(post_id)
|
||||||
is_favorite_mode_on =self .favorite_mode_checkbox .isChecked ()if self .favorite_mode_checkbox else False
|
is_favorite_mode_on =self .favorite_mode_checkbox .isChecked ()if self .favorite_mode_checkbox else False
|
||||||
|
|
||||||
|
# If the download queue contains items selected from the popup, treat it as a single-post context for UI purposes.
|
||||||
|
if self.favorite_download_queue and all(item.get('type') == 'single_post_from_popup' for item in self.favorite_download_queue):
|
||||||
|
is_single_post = True
|
||||||
|
|
||||||
|
# Allow Manga Mode checkbox for any valid URL (creator or single post) or if single posts are queued.
|
||||||
|
can_enable_manga_checkbox = (is_creator_feed or is_single_post) and not is_favorite_mode_on
|
||||||
|
|
||||||
if self .manga_mode_checkbox :
|
if self .manga_mode_checkbox :
|
||||||
self .manga_mode_checkbox .setEnabled (is_creator_feed and not is_favorite_mode_on )
|
self .manga_mode_checkbox .setEnabled (can_enable_manga_checkbox)
|
||||||
if not is_creator_feed and self .manga_mode_checkbox .isChecked ():
|
if not can_enable_manga_checkbox and self .manga_mode_checkbox .isChecked ():
|
||||||
self .manga_mode_checkbox .setChecked (False )
|
self .manga_mode_checkbox .setChecked (False )
|
||||||
checked =self .manga_mode_checkbox .isChecked ()
|
checked =self .manga_mode_checkbox .isChecked ()
|
||||||
|
|
||||||
manga_mode_effectively_on =is_creator_feed and checked
|
manga_mode_effectively_on = can_enable_manga_checkbox and checked
|
||||||
|
|
||||||
|
# If it's a single post context, prevent sequential styles from being selected as they don't apply.
|
||||||
|
sequential_styles = [STYLE_DATE_BASED, STYLE_POST_TITLE_GLOBAL_NUMBERING]
|
||||||
|
if is_single_post and self.manga_filename_style in sequential_styles:
|
||||||
|
self.manga_filename_style = STYLE_POST_TITLE # Default to a safe, non-sequential style
|
||||||
|
self._update_manga_filename_style_button_text()
|
||||||
|
# --- END: MODIFIED LOGIC ---
|
||||||
|
|
||||||
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 or is_only_audio_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 ))
|
||||||
@@ -2762,7 +2824,9 @@ class DownloaderApp (QWidget ):
|
|||||||
if total_posts >0 or processed_posts >0 :
|
if total_posts >0 or processed_posts >0 :
|
||||||
self .file_progress_label .setText ("")
|
self .file_progress_label .setText ("")
|
||||||
|
|
||||||
def start_download(self, direct_api_url=None, override_output_dir=None, is_restore=False, is_continuation=False):
|
def start_download(self, direct_api_url=None, override_output_dir=None, is_restore=False, is_continuation=False, item_type_from_queue=None):
|
||||||
|
self.finish_lock = threading.Lock()
|
||||||
|
self.is_finishing = False
|
||||||
if self.active_update_profile:
|
if self.active_update_profile:
|
||||||
if not self.new_posts_for_update:
|
if not self.new_posts_for_update:
|
||||||
return self._check_for_updates()
|
return self._check_for_updates()
|
||||||
@@ -2888,17 +2952,30 @@ class DownloaderApp (QWidget ):
|
|||||||
self.cancellation_message_logged_this_session = False
|
self.cancellation_message_logged_this_session = False
|
||||||
|
|
||||||
service, user_id, post_id_from_url = extract_post_info(api_url)
|
service, user_id, post_id_from_url = extract_post_info(api_url)
|
||||||
|
|
||||||
|
# --- START: MODIFIED SECTION ---
|
||||||
|
# This check is now smarter. It only triggers the error if the item from the queue
|
||||||
|
# was supposed to be a post ('single_post_from_popup', etc.) but couldn't be parsed.
|
||||||
|
if direct_api_url and not post_id_from_url and item_type_from_queue and 'post' in item_type_from_queue:
|
||||||
|
self.log_signal.emit(f"❌ CRITICAL ERROR: Could not parse post ID from the queued POST URL: {api_url}")
|
||||||
|
self.log_signal.emit(" Skipping this item. This might be due to an unsupported URL format or a temporary issue.")
|
||||||
|
self.download_finished(
|
||||||
|
total_downloaded=0,
|
||||||
|
total_skipped=1,
|
||||||
|
cancelled_by_user=False,
|
||||||
|
kept_original_names_list=[]
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
# --- END: MODIFIED SECTION ---
|
||||||
|
|
||||||
if not service or not user_id:
|
if not service or not user_id:
|
||||||
QMessageBox.critical(self, "Input Error", "Invalid or unsupported URL format.")
|
QMessageBox.critical(self, "Input Error", "Invalid or unsupported URL format.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Read the setting at the start of the download
|
|
||||||
self.save_creator_json_enabled_this_session = self.settings.value(SAVE_CREATOR_JSON_KEY, True, type=bool)
|
self.save_creator_json_enabled_this_session = self.settings.value(SAVE_CREATOR_JSON_KEY, True, type=bool)
|
||||||
|
|
||||||
profile_processed_ids = set() # Default to an empty set
|
creator_profile_data = {}
|
||||||
|
|
||||||
if self.save_creator_json_enabled_this_session:
|
if self.save_creator_json_enabled_this_session:
|
||||||
# --- CREATOR PROFILE LOGIC ---
|
|
||||||
creator_name_for_profile = None
|
creator_name_for_profile = None
|
||||||
if self.is_processing_favorites_queue and self.current_processing_favorite_item_info:
|
if self.is_processing_favorites_queue and self.current_processing_favorite_item_info:
|
||||||
creator_name_for_profile = self.current_processing_favorite_item_info.get('name_for_folder')
|
creator_name_for_profile = self.current_processing_favorite_item_info.get('name_for_folder')
|
||||||
@@ -2912,7 +2989,6 @@ class DownloaderApp (QWidget ):
|
|||||||
|
|
||||||
creator_profile_data = self._setup_creator_profile(creator_name_for_profile, self.session_file_path)
|
creator_profile_data = self._setup_creator_profile(creator_name_for_profile, self.session_file_path)
|
||||||
|
|
||||||
# Get all current UI settings and add them to the profile
|
|
||||||
current_settings = self._get_current_ui_settings_as_dict(api_url_override=api_url, output_dir_override=effective_output_dir_for_run)
|
current_settings = self._get_current_ui_settings_as_dict(api_url_override=api_url, output_dir_override=effective_output_dir_for_run)
|
||||||
creator_profile_data['settings'] = current_settings
|
creator_profile_data['settings'] = current_settings
|
||||||
|
|
||||||
@@ -2924,10 +3000,17 @@ class DownloaderApp (QWidget ):
|
|||||||
self._save_creator_profile(creator_name_for_profile, creator_profile_data, self.session_file_path)
|
self._save_creator_profile(creator_name_for_profile, creator_profile_data, self.session_file_path)
|
||||||
self.log_signal.emit(f"✅ Profile for '{creator_name_for_profile}' loaded/created. Settings saved.")
|
self.log_signal.emit(f"✅ Profile for '{creator_name_for_profile}' loaded/created. Settings saved.")
|
||||||
|
|
||||||
profile_processed_ids = set(creator_profile_data.get('processed_post_ids', []))
|
profile_processed_ids = set()
|
||||||
# --- END OF PROFILE LOGIC ---
|
|
||||||
|
if self.active_update_profile:
|
||||||
|
self.log_signal.emit(" Update session active: Loading existing processed post IDs to find new content.")
|
||||||
|
profile_processed_ids = set(creator_profile_data.get('processed_post_ids', []))
|
||||||
|
|
||||||
|
elif not is_restore:
|
||||||
|
self.log_signal.emit(" Fresh download session: Clearing previous post history for this creator to re-download all.")
|
||||||
|
if 'processed_post_ids' in creator_profile_data:
|
||||||
|
creator_profile_data['processed_post_ids'] = []
|
||||||
|
|
||||||
# The rest of this logic runs regardless, but uses the profile data if it was loaded
|
|
||||||
session_processed_ids = set(processed_post_ids_for_restore)
|
session_processed_ids = set(processed_post_ids_for_restore)
|
||||||
combined_processed_ids = session_processed_ids.union(profile_processed_ids)
|
combined_processed_ids = session_processed_ids.union(profile_processed_ids)
|
||||||
processed_post_ids_for_this_run = list(combined_processed_ids)
|
processed_post_ids_for_this_run = list(combined_processed_ids)
|
||||||
@@ -3055,7 +3138,7 @@ class DownloaderApp (QWidget ):
|
|||||||
elif backend_filter_mode == 'audio': current_mode_log_text = "Audio Download"
|
elif backend_filter_mode == 'audio': current_mode_log_text = "Audio Download"
|
||||||
|
|
||||||
current_char_filter_scope = self.get_char_filter_scope()
|
current_char_filter_scope = self.get_char_filter_scope()
|
||||||
manga_mode = manga_mode_is_checked and not post_id_from_url
|
manga_mode = manga_mode_is_checked
|
||||||
|
|
||||||
manga_date_prefix_text = ""
|
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'):
|
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'):
|
||||||
@@ -3478,6 +3561,7 @@ class DownloaderApp (QWidget ):
|
|||||||
if hasattr (self .download_thread ,'file_progress_signal'):self .download_thread .file_progress_signal .connect (self .update_file_progress_display )
|
if hasattr (self .download_thread ,'file_progress_signal'):self .download_thread .file_progress_signal .connect (self .update_file_progress_display )
|
||||||
if hasattr (self .download_thread ,'missed_character_post_signal'):
|
if hasattr (self .download_thread ,'missed_character_post_signal'):
|
||||||
self .download_thread .missed_character_post_signal .connect (self .handle_missed_character_post )
|
self .download_thread .missed_character_post_signal .connect (self .handle_missed_character_post )
|
||||||
|
if hasattr(self.download_thread, 'overall_progress_signal'): self.download_thread.overall_progress_signal.connect(self.update_progress_display)
|
||||||
if hasattr (self .download_thread ,'retryable_file_failed_signal'):
|
if hasattr (self .download_thread ,'retryable_file_failed_signal'):
|
||||||
|
|
||||||
if hasattr (self .download_thread ,'file_successfully_downloaded_signal'):
|
if hasattr (self .download_thread ,'file_successfully_downloaded_signal'):
|
||||||
@@ -3862,7 +3946,12 @@ class DownloaderApp (QWidget ):
|
|||||||
if not filepath.lower().endswith('.pdf'):
|
if not filepath.lower().endswith('.pdf'):
|
||||||
filepath += '.pdf'
|
filepath += '.pdf'
|
||||||
|
|
||||||
font_path = os.path.join(self.app_base_dir, 'data', 'dejavu-sans', 'DejaVuSans.ttf')
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
|
base_path = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
base_path = self.app_base_dir
|
||||||
|
|
||||||
|
font_path = os.path.join(base_path, 'data', 'dejavu-sans', 'DejaVuSans.ttf')
|
||||||
|
|
||||||
self.log_signal.emit(" Sorting collected posts by date (oldest first)...")
|
self.log_signal.emit(" Sorting collected posts by date (oldest first)...")
|
||||||
sorted_content = sorted(posts_content_data, key=lambda x: x.get('published', 'Z'))
|
sorted_content = sorted(posts_content_data, key=lambda x: x.get('published', 'Z'))
|
||||||
@@ -4182,9 +4271,12 @@ class DownloaderApp (QWidget ):
|
|||||||
# Update UI to "Cancelling" state
|
# Update UI to "Cancelling" state
|
||||||
self.pause_btn.setEnabled(False)
|
self.pause_btn.setEnabled(False)
|
||||||
self.cancel_btn.setEnabled(False)
|
self.cancel_btn.setEnabled(False)
|
||||||
|
|
||||||
|
if hasattr(self, 'reset_button'):
|
||||||
|
self.reset_button.setEnabled(False)
|
||||||
|
|
||||||
self.progress_label.setText(self._tr("status_cancelling", "Cancelling... Please wait."))
|
self.progress_label.setText(self._tr("status_cancelling", "Cancelling... Please wait."))
|
||||||
|
|
||||||
# Signal all active components to stop
|
|
||||||
if self.download_thread and self.download_thread.isRunning():
|
if self.download_thread and self.download_thread.isRunning():
|
||||||
self.download_thread.requestInterruption()
|
self.download_thread.requestInterruption()
|
||||||
self.log_signal.emit(" Signaled single download thread to interrupt.")
|
self.log_signal.emit(" Signaled single download thread to interrupt.")
|
||||||
@@ -4199,22 +4291,27 @@ class DownloaderApp (QWidget ):
|
|||||||
def _get_domain_for_service (self ,service_name :str )->str :
|
def _get_domain_for_service (self ,service_name :str )->str :
|
||||||
"""Determines the base domain for a given service."""
|
"""Determines the base domain for a given service."""
|
||||||
if not isinstance (service_name ,str ):
|
if not isinstance (service_name ,str ):
|
||||||
return "kemono.su"
|
return "kemono.cr"
|
||||||
service_lower =service_name .lower ()
|
service_lower =service_name .lower ()
|
||||||
coomer_primary_services ={'onlyfans','fansly','manyvids','candfans','gumroad','patreon','subscribestar','dlsite','discord','fantia','boosty','pixiv','fanbox'}
|
coomer_primary_services ={'onlyfans','fansly','manyvids','candfans','gumroad','patreon','subscribestar','dlsite','discord','fantia','boosty','pixiv','fanbox'}
|
||||||
if service_lower in coomer_primary_services and service_lower not in ['patreon','discord','fantia','boosty','pixiv','fanbox']:
|
if service_lower in coomer_primary_services and service_lower not in ['patreon','discord','fantia','boosty','pixiv','fanbox']:
|
||||||
return "coomer.su"
|
return "coomer.st"
|
||||||
return "kemono.su"
|
return "kemono.cr"
|
||||||
|
|
||||||
|
|
||||||
def download_finished(self, total_downloaded, total_skipped, cancelled_by_user, kept_original_names_list=None):
|
def download_finished(self, total_downloaded, total_skipped, cancelled_by_user, kept_original_names_list=None):
|
||||||
|
if not self.finish_lock.acquire(blocking=False):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
if self.is_finishing:
|
if self.is_finishing:
|
||||||
return
|
return
|
||||||
self.is_finishing = True
|
self.is_finishing = True
|
||||||
|
|
||||||
try:
|
|
||||||
if cancelled_by_user:
|
if cancelled_by_user:
|
||||||
self.log_signal.emit("✅ Cancellation complete. Resetting UI.")
|
self.log_signal.emit("✅ Cancellation complete. Resetting UI.")
|
||||||
|
self._clear_session_file()
|
||||||
|
self.interrupted_session_data = None
|
||||||
|
self.is_restore_pending = False
|
||||||
current_url = self.link_input.text()
|
current_url = self.link_input.text()
|
||||||
current_dir = self.dir_input.text()
|
current_dir = self.dir_input.text()
|
||||||
self._perform_soft_ui_reset(preserve_url=current_url, preserve_dir=current_dir)
|
self._perform_soft_ui_reset(preserve_url=current_url, preserve_dir=current_dir)
|
||||||
@@ -4222,13 +4319,14 @@ class DownloaderApp (QWidget ):
|
|||||||
self.file_progress_label.setText("")
|
self.file_progress_label.setText("")
|
||||||
if self.pause_event: self.pause_event.clear()
|
if self.pause_event: self.pause_event.clear()
|
||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
return # Exit after handling cancellation
|
return
|
||||||
|
|
||||||
self.log_signal.emit("🏁 Download of current item complete.")
|
self.log_signal.emit("🏁 Download of current item complete.")
|
||||||
|
|
||||||
if self.is_processing_favorites_queue and self.favorite_download_queue:
|
if self.is_processing_favorites_queue and self.favorite_download_queue:
|
||||||
self.log_signal.emit("✅ Item finished. Processing next in queue...")
|
self.log_signal.emit("✅ Item finished. Processing next in queue...")
|
||||||
self.is_finishing = False # Allow the next item in queue to start
|
self.is_finishing = False
|
||||||
|
self.finish_lock.release()
|
||||||
self._process_next_favorite_download()
|
self._process_next_favorite_download()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -4317,6 +4415,7 @@ class DownloaderApp (QWidget ):
|
|||||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
self.is_finishing = False # Allow retry session to start
|
self.is_finishing = False # Allow retry session to start
|
||||||
|
self.finish_lock.release() # Release lock for the retry session
|
||||||
self._start_failed_files_retry_session()
|
self._start_failed_files_retry_session()
|
||||||
return # Exit to allow retry session to run
|
return # Exit to allow retry session to run
|
||||||
else:
|
else:
|
||||||
@@ -4334,7 +4433,7 @@ class DownloaderApp (QWidget ):
|
|||||||
self.cancellation_message_logged_this_session = False
|
self.cancellation_message_logged_this_session = False
|
||||||
self.active_update_profile = None
|
self.active_update_profile = None
|
||||||
finally:
|
finally:
|
||||||
self.is_finishing = False
|
pass
|
||||||
|
|
||||||
def _handle_keep_duplicates_toggled(self, checked):
|
def _handle_keep_duplicates_toggled(self, checked):
|
||||||
"""Shows the duplicate handling dialog when the checkbox is checked."""
|
"""Shows the duplicate handling dialog when the checkbox is checked."""
|
||||||
@@ -5164,6 +5263,31 @@ class DownloaderApp (QWidget ):
|
|||||||
if hasattr(self, 'link_input'):
|
if hasattr(self, 'link_input'):
|
||||||
self.last_link_input_text_for_queue_sync = self.link_input.text()
|
self.last_link_input_text_for_queue_sync = self.link_input.text()
|
||||||
|
|
||||||
|
# --- START: MODIFIED LOGIC ---
|
||||||
|
# Manually trigger the UI update now that the queue is populated and the dialog is closed.
|
||||||
|
self.update_ui_for_manga_mode(self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False)
|
||||||
|
# --- END: MODIFIED LOGIC ---
|
||||||
|
|
||||||
|
def _load_saved_cookie_settings(self):
|
||||||
|
"""Loads and applies saved cookie settings on startup."""
|
||||||
|
try:
|
||||||
|
use_cookie_saved = self.settings.value(USE_COOKIE_KEY, False, type=bool)
|
||||||
|
cookie_content_saved = self.settings.value(COOKIE_TEXT_KEY, "", type=str)
|
||||||
|
|
||||||
|
if use_cookie_saved and cookie_content_saved:
|
||||||
|
self.use_cookie_checkbox.setChecked(True)
|
||||||
|
self.cookie_text_input.setText(cookie_content_saved)
|
||||||
|
|
||||||
|
# Check if the saved content is a file path and update UI accordingly
|
||||||
|
if os.path.exists(cookie_content_saved):
|
||||||
|
self.selected_cookie_filepath = cookie_content_saved
|
||||||
|
self.cookie_text_input.setReadOnly(True)
|
||||||
|
self._update_cookie_input_placeholders_and_tooltips()
|
||||||
|
|
||||||
|
self.log_signal.emit(f"ℹ️ Loaded saved cookie settings.")
|
||||||
|
except Exception as e:
|
||||||
|
self.log_signal.emit(f"⚠️ Could not load saved cookie settings: {e}")
|
||||||
|
|
||||||
def _show_favorite_artists_dialog (self ):
|
def _show_favorite_artists_dialog (self ):
|
||||||
if self ._is_download_active ()or self .is_processing_favorites_queue :
|
if self ._is_download_active ()or self .is_processing_favorites_queue :
|
||||||
QMessageBox .warning (self ,"Busy","Another download operation is already in progress.")
|
QMessageBox .warning (self ,"Busy","Another download operation is already in progress.")
|
||||||
@@ -5330,8 +5454,10 @@ class DownloaderApp (QWidget ):
|
|||||||
next_url =self .current_processing_favorite_item_info ['url']
|
next_url =self .current_processing_favorite_item_info ['url']
|
||||||
item_display_name =self .current_processing_favorite_item_info .get ('name','Unknown Item')
|
item_display_name =self .current_processing_favorite_item_info .get ('name','Unknown Item')
|
||||||
|
|
||||||
|
# --- START: MODIFIED SECTION ---
|
||||||
|
# Get the type of item from the queue to help start_download make smarter decisions.
|
||||||
item_type = self.current_processing_favorite_item_info.get('type', 'artist')
|
item_type = self.current_processing_favorite_item_info.get('type', 'artist')
|
||||||
self .log_signal .emit (f"▶️ Processing next favorite from queue: '{item_display_name }' ({next_url })")
|
self.log_signal.emit(f"▶️ Processing next favorite from queue ({item_type}): '{item_display_name}' ({next_url})")
|
||||||
|
|
||||||
override_dir = None
|
override_dir = None
|
||||||
item_scope = self.current_processing_favorite_item_info.get('scope_from_popup')
|
item_scope = self.current_processing_favorite_item_info.get('scope_from_popup')
|
||||||
@@ -5352,11 +5478,19 @@ class DownloaderApp (QWidget ):
|
|||||||
override_dir = os.path.normpath(os.path.join(main_download_dir, item_specific_folder_name))
|
override_dir = os.path.normpath(os.path.join(main_download_dir, item_specific_folder_name))
|
||||||
self.log_signal.emit(f" Scope requires artist folder. Target directory: '{override_dir}'")
|
self.log_signal.emit(f" Scope requires artist folder. Target directory: '{override_dir}'")
|
||||||
|
|
||||||
success_starting_download =self .start_download (direct_api_url =next_url ,override_output_dir =override_dir, is_continuation=True )
|
# Pass the item_type to the start_download function
|
||||||
|
success_starting_download = self.start_download(
|
||||||
|
direct_api_url=next_url,
|
||||||
|
override_output_dir=override_dir,
|
||||||
|
is_continuation=True,
|
||||||
|
item_type_from_queue=item_type
|
||||||
|
)
|
||||||
|
# --- END: MODIFIED SECTION ---
|
||||||
|
|
||||||
if not success_starting_download:
|
if not success_starting_download:
|
||||||
self .log_signal .emit (f"⚠️ Failed to initiate download for '{item_display_name }'. Skipping this item in queue.")
|
self.log_signal.emit(f"⚠️ Failed to initiate download for '{item_display_name}'. Skipping and moving to the next item in queue.")
|
||||||
self .download_finished (total_downloaded =0 ,total_skipped =1 ,cancelled_by_user =True ,kept_original_names_list =[])
|
# Use a QTimer to avoid deep recursion and correctly move to the next item.
|
||||||
|
QTimer.singleShot(100, self._process_next_favorite_download)
|
||||||
|
|
||||||
class ExternalLinkDownloadThread (QThread ):
|
class ExternalLinkDownloadThread (QThread ):
|
||||||
"""A QThread to handle downloading multiple external links sequentially."""
|
"""A QThread to handle downloading multiple external links sequentially."""
|
||||||
|
|||||||
@@ -196,10 +196,9 @@ def get_link_platform(url):
|
|||||||
if 'twitter.com' in domain or 'x.com' in domain: return 'twitter/x'
|
if 'twitter.com' in domain or 'x.com' in domain: return 'twitter/x'
|
||||||
if 'discord.gg' in domain or 'discord.com/invite' in domain: return 'discord invite'
|
if 'discord.gg' in domain or 'discord.com/invite' in domain: return 'discord invite'
|
||||||
if 'pixiv.net' in domain: return 'pixiv'
|
if 'pixiv.net' in domain: return 'pixiv'
|
||||||
if 'kemono.su' in domain or 'kemono.party' in domain: return 'kemono'
|
if 'kemono.su' in domain or 'kemono.party' in domain or 'kemono.cr' in domain: return 'kemono'
|
||||||
if 'coomer.su' in domain or 'coomer.party' in domain: return 'coomer'
|
if 'coomer.su' in domain or 'coomer.party' in domain or 'coomer.st' in domain: return 'coomer'
|
||||||
|
|
||||||
# Fallback to a generic name for other domains
|
|
||||||
parts = domain.split('.')
|
parts = domain.split('.')
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
return parts[-2]
|
return parts[-2]
|
||||||
|
|||||||
Reference in New Issue
Block a user