7 Commits

Author SHA1 Message Date
Yuvi9587
661b97aa16 Commit 2025-08-06 06:56:49 -07:00
Yuvi9587
3704fece2b Update main_window.py 2025-08-04 04:53:52 -07:00
Yuvi9587
bdb7ac93c4 Update readme.md 2025-08-03 09:16:25 -07:00
Yuvi9587
76d4a3ea8a Update main_window.py 2025-08-03 09:15:01 -07:00
Yuvi9587
ccc7804505 Update readme.md 2025-08-03 09:13:47 -07:00
Yuvi9587
4ee750c5d4 Update drive_downloader.py 2025-08-03 09:11:27 -07:00
Yuvi9587
e9be13c4e3 Update readme.md 2025-08-03 09:07:29 -07:00
4 changed files with 78 additions and 243 deletions

145
readme.md
View File

@@ -1,4 +1,4 @@
<h1 align="center">Kemono Downloader v6.0.0</h1> <h1 align="center">Kemono Downloader </h1>
<div align="center"> <div align="center">
@@ -41,108 +41,53 @@ Built with PyQt5, this tool is designed for users who want deep filtering capabi
</div> </div>
<h2><strong>Core Capabilities Overview</strong></h2>
--- <h3><strong>High-Performance Downloading</strong></h3>
<ul>
<li><strong>Multi-threading:</strong> Processes multiple posts simultaneously to greatly accelerate downloads from large creator profiles.</li>
<li><strong>Multi-part Downloading:</strong> Splits large files into chunks and downloads them in parallel to maximize speed.</li>
<li><strong>Resilience:</strong> Supports pausing, resuming, and restoring downloads after crashes or interruptions.</li>
</ul>
## Feature Overview <h3><strong>Advanced Filtering & Content Control</strong></h3>
<ul>
<li><strong>Content Type Filtering:</strong> Select whether to download all files or limit to images, videos, audio, or archives only.</li>
<li><strong>Keyword Skipping:</strong> Automatically skips posts or files containing certain keywords (e.g., "WIP", "sketch").</li>
<li><strong>Character Filtering:</strong> Restricts downloads to posts that match specific character or series names.</li>
</ul>
Kemono Downloader offers a range of features to streamline your content downloading experience: <h3><strong>File Organization & Renaming</strong></h3>
<ul>
<li><strong>Automated Subfolders:</strong> Automatically organizes downloaded files into subdirectories based on character names or per post.</li>
<li><strong>Advanced File Renaming:</strong> Flexible renaming options, especially in Manga Mode, including:
<ul>
<li><strong>Post Title:</strong> Uses the post's title (e.g., <code>Chapter-One.jpg</code>).</li>
<li><strong>Date + Original Name:</strong> Prepends the publication date to the original filename.</li>
<li><strong>Date + Title:</strong> Combines the date with the post title.</li>
<li><strong>Sequential Numbering (Date Based):</strong> Simple sequence numbers (e.g., <code>001.jpg</code>, <code>002.jpg</code>).</li>
<li><strong>Title + Global Numbering:</strong> Uses post title with a globally incrementing number across the session.</li>
<li><strong>Post ID:</strong> Names files using the posts unique ID.</li>
</ul>
</li>
</ul>
- **User-Friendly Interface:** A modern PyQt5 GUI for easy navigation and operation. <h3><strong>Specialized Modes</strong></h3>
<ul>
<li><strong>Manga/Comic Mode:</strong> Sorts posts chronologically before downloading to ensure pages appear in the correct sequence.</li>
<li><strong>Favorite Mode:</strong> Connects to your account and downloads from your favorites list (artists or posts).</li>
<li><strong>Link Extraction Mode:</strong> Extracts external links from posts for export or targeted downloading.</li>
<li><strong>Text Extraction Mode:</strong> Saves post descriptions or comment sections as <code>PDF</code>, <code>DOCX</code>, or <code>TXT</code> files.</li>
</ul>
- **Flexible Downloading:** <h3><strong>Utility & Advanced Features</strong></h3>
- Download content from Kemono.su (and mirrors) and Coomer.party (and mirrors). <ul>
- Supports creator pages (with page range selection) and individual post URLs. <li><strong>Cookie Support:</strong> Enables access to subscriber-only content via browser session cookies.</li>
- Standard download controls: Start, Pause, Resume, and Cancel. <li><strong>Duplicate Detection:</strong> Prevents saving duplicate files using content-based comparison, with configurable limits.</li>
<li><strong>Image Compression:</strong> Automatically converts large images to <code>.webp</code> to reduce disk usage.</li>
- **Powerful Filtering:** <li><strong>Creator Management:</strong> Built-in creator browser and update checker for downloading only new posts from saved profiles.</li>
- **Character Filtering:** Filter content by character names. Supports simple comma-separated names and grouped names for shared folders. <li><strong>Error Handling:</strong> Tracks failed downloads and provides a retry dialog with options to export or redownload missing files.</li>
- **Keyword Skipping:** Skip posts or files based on specified keywords. </ul>
- **Filename Cleaning:** Remove unwanted words or phrases from downloaded filenames.
- **File Type Selection:** Choose to download all files, or limit to images/GIFs, videos, audio, or archives. Can also extract external links only.
- **Customizable Downloads:**
- **Thumbnails Only:** Option to download only small preview images.
- **Content Scanning:** Scan post HTML for `<img>` tags and direct image links, useful for images embedded in descriptions.
- **WebP Conversion:** Convert images to WebP format for smaller file sizes (requires Pillow library).
- **Organized Output:**
- **Automatic Subfolders:** Create subfolders based on character names (from filters or `Known.txt`) or post titles.
- **Per-Post Subfolders:** Option to create an additional subfolder for each individual post.
- **Manga/Comic Mode:**
- Downloads posts from a creator's feed in chronological order (oldest to newest).
- Offers various filename styling options for sequential reading (e.g., post title, original name, global numbering).
- **⭐ Favorite Mode:**
- Directly download from your favorited artists and posts on Kemono.su.
- Requires a valid cookie and adapts the UI for easy selection from your favorites.
- Supports downloading into a single location or artist-specific subfolders.
- **Performance & Advanced Options:**
- **Cookie Support:** Use cookies (paste string or load from `cookies.txt`) to access restricted content.
- **Multithreading:** Configure the number of simultaneous downloads/post processing threads for improved speed.
- **Logging:**
- A detailed progress log displays download activity, errors, and summaries.
- **Multi-language Interface:** Choose from several languages for the UI (English, Japanese, French, Spanish, German, Russian, Korean, Chinese Simplified).
- **Theme Customization:** Selectable Light and Dark themes for user comfort.
---
## ✨ What's New in v6.0.0
This release focuses on providing more granular control over file organization and improving at-a-glance status monitoring.
### New Features
- **Live Error Count on Button**
The **"Error" button** now dynamically displays the number of failed files during a download. Instead of opening the dialog, you can quickly see a live count like `(3) Error`, helping you track issues at a glance.
- **Date Prefix for Post Subfolders**
A new checkbox labeled **"Date Prefix"** is now available in the advanced settings.
When enabled alongside **"Subfolder per Post"**, it prepends the post's upload date to the folder name (e.g., `2025-07-11 Post Title`).
This makes your downloads sortable and easier to browse chronologically.
- **Keep Duplicates Within a Post**
A **"Keep Duplicates"** option has been added to preserve all files from a post — even if some have the same name.
Instead of skipping or overwriting, the downloader will save duplicates with numbered suffixes (e.g., `image.jpg`, `image_1.jpg`, etc.), which is especially useful when the same file name points to different media.
### Bug Fixes
- The downloader now correctly renames large `.part` files when completed, avoiding leftover temp files.
- The list of failed files shown in the Error Dialog is now saved and restored with your session — so no errors get lost if you close the app.
- Your selected download location is remembered, even after pressing the **Reset** button.
- The **Cancel** button is now enabled when restoring a pending session, so you can abort stuck jobs more easily.
- Internal cleanup logs (like "Deleting post cache") are now excluded from the final download summary for clarity.
---
## 📅 Next Update Plans
### 🔖 Post Tag Filtering (Planned for v6.1.0)
A powerful new **"Filter by Post Tags"** feature is planned:
- Filter and download content based on specific post tags.
- Combine tag filtering with current filters (character, file type, etc.).
- Use tag presets to automate frequent downloads.
This will provide **much greater control** over what gets downloaded, especially for creators who use tags consistently.
### 📁 Creator Download History (.json Save)
To streamline incremental downloads, a new system will allow the app to:
- Save a `.json` file with metadata about already-downloaded posts.
- Compare that file on future runs, so only **new** posts are downloaded.
- Avoids duplication and makes regular syncs fast and efficient.
Ideal for users managing large collections or syncing favorites regularly.
---
## 💻 Installation ## 💻 Installation
@@ -154,7 +99,7 @@ Ideal for users managing large collections or syncing favorites regularly.
### Install Dependencies ### Install Dependencies
```bash ```bash
pip install PyQt5 requests Pillow mega.py pip install PyQt5 requests Pillow mega.py fpdf2 python-docx
``` ```
### Running the Application ### Running the Application
@@ -197,7 +142,7 @@ Feel free to fork this repo and submit pull requests for bug fixes, new features
## License ## License
This project is under the Custom Licence This project is under the MIT Licence
## Star History ## Star History

View File

@@ -97,7 +97,7 @@ FOLDER_NAME_STOP_WORDS = {
"for", "he", "her", "his", "i", "im", "in", "is", "it", "its", "for", "he", "her", "his", "i", "im", "in", "is", "it", "its",
"me", "my", "net", "not", "of", "on", "or", "org", "our", "me", "my", "net", "not", "of", "on", "or", "org", "our",
"s", "she", "so", "the", "their", "they", "this", "s", "she", "so", "the", "their", "they", "this",
"to", "ve", "was", "we", "were", "with", "www", "you", "your", "to", "ve", "was", "we", "were", "with", "www", "you", "your", "nsfw", "sfw",
# add more according to need # add more according to need
} }

View File

@@ -7,8 +7,6 @@ import base64
import time import time
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
# --- Third-Party Library Imports ---
# Make sure to install these: pip install requests pycryptodome gdown
import requests import requests
try: try:
@@ -23,11 +21,8 @@ try:
except ImportError: except ImportError:
GDRIVE_AVAILABLE = False GDRIVE_AVAILABLE = False
# --- Constants ---
MEGA_API_URL = "https://g.api.mega.co.nz" MEGA_API_URL = "https://g.api.mega.co.nz"
# --- Helper Functions (Original and New) ---
def _get_filename_from_headers(headers): def _get_filename_from_headers(headers):
""" """
Extracts a filename from the Content-Disposition header. Extracts a filename from the Content-Disposition header.

View File

@@ -272,7 +272,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.3.0") self.setWindowTitle("Kemono Downloader v6.3.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.")
@@ -626,7 +626,7 @@ class DownloaderApp (QWidget ):
self.log_signal.emit(" Update selection cleared. Resetting UI to defaults.") self.log_signal.emit(" Update selection cleared. Resetting UI to defaults.")
self.active_update_profile = None self.active_update_profile = None
self.new_posts_for_update = [] self.new_posts_for_update = []
self._reset_ui_to_defaults() self._perform_soft_ui_reset()
def _retranslate_main_ui (self ): def _retranslate_main_ui (self ):
"""Retranslates static text elements in the main UI.""" """Retranslates static text elements in the main UI."""
@@ -4715,12 +4715,10 @@ class DownloaderApp (QWidget ):
def reset_application_state(self): def reset_application_state(self):
self.log_signal.emit("🔄 Resetting application state to defaults...") self.log_signal.emit("🔄 Resetting application state to defaults...")
# --- MODIFIED PART: Signal all threads to stop first, but do not wait ---
if self._is_download_active(): if self._is_download_active():
self.log_signal.emit(" Cancelling all active background tasks for reset...") self.log_signal.emit(" Cancelling all active background tasks for reset...")
self.cancellation_event.set() # Signal all threads to stop self.cancellation_event.set()
# Initiate non-blocking shutdowns
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()
if self.thread_pool: if self.thread_pool:
@@ -4732,8 +4730,7 @@ class DownloaderApp (QWidget ):
self.retry_thread_pool.shutdown(wait=False, cancel_futures=True) self.retry_thread_pool.shutdown(wait=False, cancel_futures=True)
self.retry_thread_pool = None self.retry_thread_pool = None
self.cancellation_event.clear() # Reset the event for the next run self.cancellation_event.clear()
# --- END OF MODIFIED PART ---
if self.pause_event: if self.pause_event:
self.pause_event.clear() self.pause_event.clear()
@@ -4806,123 +4803,6 @@ class DownloaderApp (QWidget ):
self.is_restore_pending = False self.is_restore_pending = False
self.last_link_input_text_for_queue_sync = "" self.last_link_input_text_for_queue_sync = ""
def _reset_ui_to_defaults(self):
"""Resets all UI elements and relevant state to their default values."""
self.link_input.clear()
self.custom_folder_input.clear()
self.character_input.clear()
self.skip_words_input.clear()
self.start_page_input.clear()
self.end_page_input.clear()
self.new_char_input.clear()
if hasattr(self, 'remove_from_filename_input'):
self.remove_from_filename_input.clear()
self.character_search_input.clear()
self.thread_count_input.setText("4")
if hasattr(self, 'manga_date_prefix_input'):
self.manga_date_prefix_input.clear()
self.radio_all.setChecked(True)
self.skip_zip_checkbox.setChecked(True)
self.download_thumbnails_checkbox.setChecked(False)
self.compress_images_checkbox.setChecked(False)
self.use_subfolders_checkbox.setChecked(False)
self.use_subfolder_per_post_checkbox.setChecked(True)
self.use_multithreading_checkbox.setChecked(True)
if self.favorite_mode_checkbox:
self.favorite_mode_checkbox.setChecked(False)
if hasattr(self, 'keep_duplicates_checkbox'):
self.keep_duplicates_checkbox.setChecked(False)
self.external_links_checkbox.setChecked(False)
if self.manga_mode_checkbox:
self.manga_mode_checkbox.setChecked(False)
if hasattr(self, 'use_cookie_checkbox'):
self.use_cookie_checkbox.setChecked(False)
self.selected_cookie_filepath = None
if hasattr(self, 'cookie_text_input'):
self.cookie_text_input.clear()
if self.main_log_output:
self.main_log_output.clear()
if self.external_log_output:
self.external_log_output.clear()
if self.missed_character_log_output:
self.missed_character_log_output.clear()
self.progress_label.setText(self._tr("progress_idle_text", "Progress: Idle"))
self.file_progress_label.setText("")
self.missed_title_key_terms_count.clear()
self.missed_title_key_terms_examples.clear()
self.logged_summary_for_key_term.clear()
self.already_logged_bold_key_terms.clear()
self.missed_key_terms_buffer.clear()
self.permanently_failed_files_for_dialog.clear()
self.only_links_log_display_mode = LOG_DISPLAY_LINKS
self.cancellation_message_logged_this_session = False
self.mega_download_log_preserved_once = False
self.allow_multipart_download_setting = False
self.skip_words_scope = SKIP_SCOPE_POSTS
self.char_filter_scope = CHAR_SCOPE_TITLE
self.manga_filename_style = STYLE_POST_TITLE
self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION
self._update_skip_scope_button_text()
self._update_char_filter_scope_button_text()
self._update_manga_filename_style_button_text()
self._update_multipart_toggle_button_text()
self._update_favorite_scope_button_text()
self.current_log_view = 'progress'
self.is_paused = False
if self.pause_event:
self.pause_event.clear()
self.external_link_queue.clear()
self.extracted_links_cache = []
self._is_processing_external_link_queue = False
self._current_link_post_title = None
if self.download_extracted_links_button:
self.download_extracted_links_button.setEnabled(False)
self.favorite_download_queue.clear()
self.is_processing_favorites_queue = False
self.current_processing_favorite_item_info = None
self.interrupted_session_data = None
self.is_restore_pending = False
self.last_link_input_text_for_queue_sync = ""
self._update_button_states_and_connections()
self.total_posts_to_process = 0
self.processed_posts_count = 0
self.download_counter = 0
self.skip_counter = 0
self.all_kept_original_filenames = []
if self.log_view_stack:
self.log_view_stack.setCurrentIndex(0)
if self.progress_log_label:
self.progress_log_label.setText(self._tr("progress_log_label_text", "📜 Progress Log:"))
if self.log_verbosity_toggle_button:
self.log_verbosity_toggle_button.setText(self.EYE_ICON)
self.log_verbosity_toggle_button.setToolTip("Current View: Progress Log. Click to switch to Missed Character Log.")
self.filter_character_list("")
self._handle_multithreading_toggle(self.use_multithreading_checkbox.isChecked())
self.update_ui_for_manga_mode(False)
self.update_custom_folder_visibility(self.link_input.text())
self.update_page_range_enabled_state()
self._update_cookie_input_visibility(False)
self._update_cookie_input_placeholders_and_tooltips()
self.download_btn.setEnabled(True)
self.cancel_btn.setEnabled(False)
if self.reset_button:
self.reset_button.setEnabled(True)
self.reset_button.setText(self._tr("reset_button_text", "🔄 Reset"))
self.reset_button.setToolTip(self._tr("reset_button_tooltip", "Reset all inputs and logs to default state (only when idle)."))
if hasattr(self, 'favorite_mode_checkbox'):
self._handle_favorite_mode_toggle(False)
if hasattr(self, 'scan_content_images_checkbox'):
self.scan_content_images_checkbox.setChecked(False)
if hasattr(self, 'download_thumbnails_checkbox'):
self._handle_thumbnail_mode_change(self.download_thumbnails_checkbox.isChecked())
self.set_ui_enabled(True)
self.log_signal.emit("✅ UI reset to defaults. Ready for new operation.")
self._update_button_states_and_connections()
def _show_feature_guide (self ): def _show_feature_guide (self ):
steps_content_keys =[ steps_content_keys =[
("help_guide_step1_title","help_guide_step1_content"), ("help_guide_step1_title","help_guide_step1_content"),
@@ -5122,7 +5002,8 @@ 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.")
self.new_posts_for_update = [post for post in all_posts_from_api if post.get('id') not in processed_ids_from_profile] # CORRECTED LINE: Assign the list directly without re-filtering
self.new_posts_for_update = all_posts_from_api
if not self.new_posts_for_update: if not self.new_posts_for_update:
self.log_signal.emit("✅ Creator is up to date! No new posts found.") self.log_signal.emit("✅ Creator is up to date! No new posts found.")
@@ -5145,22 +5026,29 @@ class DownloaderApp (QWidget ):
update_url = self.active_update_profile['creator_url'][0] update_url = self.active_update_profile['creator_url'][0]
service, user_id, _ = extract_post_info(update_url) service, user_id, _ = extract_post_info(update_url)
# --- FIX: Use the BASE download path, not the creator-specific one ---
# Get the base path from the UI (e.g., "E:/Kemono"). The worker will create subfolders inside this.
base_download_dir_from_ui = self.dir_input.text().strip() base_download_dir_from_ui = self.dir_input.text().strip()
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}")
# --- END FIX ---
raw_character_filters_text = self.character_input.text().strip() raw_character_filters_text = self.character_input.text().strip()
parsed_character_filter_objects = self._parse_character_filters(raw_character_filters_text) parsed_character_filter_objects = self._parse_character_filters(raw_character_filters_text)
# --- FIX: Set paths to mimic a normal download, allowing the worker to create subfolders --- try:
# 'download_root' is the base directory. num_threads_from_gui = int(self.thread_count_input.text().strip())
# 'override_output_dir' is None, which allows the worker to use its own folder logic. except ValueError:
num_threads_from_gui = 1
effective_num_file_threads_per_worker = max(1, min(num_threads_from_gui, MAX_FILE_THREADS_PER_POST_OR_WORKER))
# Logic to get folder ignore words if no character filters are used
creator_folder_ignore_words_for_run = None
# The 'not self.active_update_profile.get('post_id')' part is a simple way to check if it's a full creator page
is_full_creator_download = not self.active_update_profile.get('post_id')
if is_full_creator_download and not parsed_character_filter_objects:
creator_folder_ignore_words_for_run = CREATOR_DOWNLOAD_DEFAULT_FOLDER_IGNORE_WORDS
args_template = { args_template = {
'api_url_input': update_url, 'api_url_input': update_url,
'download_root': base_download_dir_from_ui, # Corrected: Use the BASE path 'download_root': base_download_dir_from_ui,
'override_output_dir': None, # Corrected: Set to None to allow subfolder logic 'override_output_dir': None,
'known_names': list(KNOWN_NAMES), 'known_names': list(KNOWN_NAMES),
'filter_character_list': parsed_character_filter_objects, 'filter_character_list': parsed_character_filter_objects,
'emitter': self.worker_to_gui_queue, 'emitter': self.worker_to_gui_queue,
@@ -5197,7 +5085,13 @@ class DownloaderApp (QWidget ):
'manga_date_prefix': self.manga_date_prefix_input.text().strip(), 'manga_date_prefix': self.manga_date_prefix_input.text().strip(),
'manga_date_file_counter_ref': None, 'manga_date_file_counter_ref': None,
'scan_content_for_images': self.scan_content_images_checkbox.isChecked(), 'scan_content_for_images': self.scan_content_images_checkbox.isChecked(),
'creator_download_folder_ignore_words': None,
'creator_download_folder_ignore_words': creator_folder_ignore_words_for_run,
'num_file_threads_for_worker': effective_num_file_threads_per_worker,
'multipart_scope': 'files',
'multipart_parts_count': 8,
'multipart_min_size_mb': 100,
'manga_global_file_counter_ref': None, 'manga_global_file_counter_ref': None,
'use_date_prefix_for_subfolder': self.date_prefix_checkbox.isChecked(), 'use_date_prefix_for_subfolder': self.date_prefix_checkbox.isChecked(),
'keep_in_post_duplicates': self.keep_duplicates_checkbox.isChecked(), 'keep_in_post_duplicates': self.keep_duplicates_checkbox.isChecked(),
@@ -5211,7 +5105,8 @@ class DownloaderApp (QWidget ):
'text_export_format': self.text_export_format, 'text_export_format': self.text_export_format,
'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']),
'keep_archives_skip_others': self.keep_archives_skip_others_checkbox.isChecked() if hasattr(self, 'keep_archives_skip_others_checkbox') else False
} }
num_threads = int(self.thread_count_input.text()) if self.use_multithreading_checkbox.isChecked() else 1 num_threads = int(self.thread_count_input.text()) if self.use_multithreading_checkbox.isChecked() else 1