diff --git a/src/core/api_client.py b/src/core/api_client.py index 4263c67..55e4d4a 100644 --- a/src/core/api_client.py +++ b/src/core/api_client.py @@ -41,9 +41,14 @@ def fetch_posts_paginated(api_url_base, headers, offset, logger, cancellation_ev try: response = requests.get(paginated_url, headers=headers, timeout=(15, 60), cookies=cookies_dict) response.raise_for_status() + response.encoding = 'utf-8' return response.json() except requests.exceptions.RequestException as e: + if e.response is not None and e.response.status_code == 400: + logger(f" ✅ Reached end of posts (API returned 400 Bad Request for offset {offset}).") + return [] + logger(f" ⚠️ Retryable network error on page fetch (Attempt {attempt + 1}): {e}") if attempt < max_retries - 1: delay = retry_delay * (2 ** attempt) @@ -81,9 +86,12 @@ def fetch_single_post_data(api_domain, service, user_id, post_id, headers, logge response_body += chunk full_post_data = json.loads(response_body) + if isinstance(full_post_data, list) and full_post_data: - return full_post_data[0] - return full_post_data + return full_post_data[0] + if isinstance(full_post_data, dict) and 'post' in full_post_data: + return full_post_data['post'] + return full_post_data except Exception as e: logger(f" ❌ Failed to fetch full content for post {post_id}: {e}") @@ -101,6 +109,7 @@ def fetch_post_comments(api_domain, service, user_id, post_id, headers, logger, try: response = requests.get(comments_api_url, headers=headers, timeout=(10, 30), cookies=cookies_dict) response.raise_for_status() + response.encoding = 'utf-8' return response.json() except requests.exceptions.RequestException as e: raise RuntimeError(f"Error fetching comments for post {post_id}: {e}") @@ -141,12 +150,9 @@ def download_from_api( parsed_input_url_for_domain = urlparse(api_url_input) api_domain = parsed_input_url_for_domain.netloc - # --- START: MODIFIED LOGIC --- - # This list is updated to include the new .cr and .st mirrors for validation. if 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}' from input URL. Defaulting to kemono.su for API calls.") api_domain = "kemono.su" - # --- END: MODIFIED LOGIC --- cookies_for_api = None if use_cookie and app_base_dir: @@ -160,6 +166,7 @@ def download_from_api( try: direct_response = requests.get(direct_post_api_url, headers=headers, timeout=(10, 30), cookies=cookies_for_api) direct_response.raise_for_status() + direct_response.encoding = 'utf-8' direct_post_data = direct_response.json() if isinstance(direct_post_data, list) and direct_post_data: direct_post_data = direct_post_data[0] diff --git a/src/core/workers.py b/src/core/workers.py index 6881aec..8ae510b 100644 --- a/src/core/workers.py +++ b/src/core/workers.py @@ -37,7 +37,7 @@ try: except ImportError: Document = None from PyQt5 .QtCore import Qt ,QThread ,pyqtSignal ,QMutex ,QMutexLocker ,QObject ,QTimer ,QSettings ,QStandardPaths ,QCoreApplication ,QUrl ,QSize ,QProcess -from .api_client import download_from_api, fetch_post_comments +from .api_client import download_from_api, fetch_post_comments, fetch_single_post_data from ..services.multipart_downloader import download_file_in_parts, MULTIPART_DOWNLOADER_AVAILABLE from ..services.drive_downloader import ( download_mega_file, download_gdrive_file, download_dropbox_file @@ -876,6 +876,37 @@ class PostProcessorWorker: post_data = self.post # Reference to the post object log_prefix = "Post" + # --- FIX: FETCH FULL POST DATA IF CONTENT IS MISSING BUT NEEDED --- + content_is_needed = ( + self.show_external_links or + self.extract_links_only or + self.scan_content_for_images or + (self.filter_mode == 'text_only' and self.text_only_scope == 'content') + ) + + if content_is_needed and self.post.get('content') is None and self.service != 'discord': + self.logger(f" Post {post_id} is missing 'content' field, fetching full data...") + parsed_url = urlparse(self.api_url_input) + api_domain = parsed_url.netloc + headers = {'User-Agent': 'Mozilla/5.0'} + cookies = prepare_cookies_for_request(self.use_cookie, self.cookie_text, self.selected_cookie_file, self.app_base_dir, self.logger, target_domain=api_domain) + + full_post_data = fetch_single_post_data(api_domain, self.service, self.user_id, post_id, headers, self.logger, cookies_dict=cookies) + + if full_post_data: + self.logger(" ✅ Full post data fetched successfully.") + # Update the worker's post object with the complete data + self.post = full_post_data + # Re-initialize local variables from the new, complete post data + post_title = self.post.get('title', '') or 'untitled_post' + post_main_file_info = self.post.get('file') + post_attachments = self.post.get('attachments', []) + post_content_html = self.post.get('content', '') + post_data = self.post + else: + self.logger(f" ⚠️ Failed to fetch full content for post {post_id}. Content-dependent features may not work for this post.") + # --- END FIX --- + # 2. SHARED PROCESSING LOGIC: The rest of the function now uses the consistent variables from above. result_tuple = (0, 0, [], [], [], None, None) total_downloaded_this_post = 0 @@ -1286,7 +1317,6 @@ class PostProcessorWorker: parsed_url = urlparse(self.api_url_input) api_domain = parsed_url.netloc cookies = prepare_cookies_for_request(self.use_cookie, self.cookie_text, self.selected_cookie_file, self.app_base_dir, self.logger, target_domain=api_domain) - from .api_client import fetch_single_post_data full_data = fetch_single_post_data(api_domain, self.service, self.user_id, post_id, headers, self.logger, cookies_dict=cookies) if full_data: final_post_data = full_data diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 850c16b..43a4f3d 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -281,7 +281,7 @@ class DownloaderApp (QWidget ): self.download_location_label_widget = None self.remove_from_filename_label_widget = None self.skip_words_label_widget = None - self.setWindowTitle("Kemono Downloader v6.4.1") + self.setWindowTitle("Kemono Downloader v6.4.2") setup_ui(self) self._connect_signals() self.log_signal.emit("ℹ️ Local API server functionality has been removed.")