From bcf26bea202c69a5243b560eb27d216c18574f84 Mon Sep 17 00:00:00 2001
From: Yuvi9587 <114073886+Yuvi9587@users.noreply.github.com>
Date: Fri, 11 Jul 2025 01:24:12 -0700
Subject: [PATCH] Commit
---
features.md | 262 +++++++++++++++---------------
readme.md | 182 +++++++++++----------
src/core/workers.py | 262 +++++++++++++++++++++++-------
src/ui/dialogs/HelpGuideDialog.py | 2 +-
src/ui/main_window.py | 67 ++++++--
5 files changed, 482 insertions(+), 293 deletions(-)
diff --git a/features.md b/features.md
index 88cdad6..329ec37 100644
--- a/features.md
+++ b/features.md
@@ -5,186 +5,188 @@ This guide provides a comprehensive overview of all user interface elements, inp
These are the primary controls you'll interact with to initiate and manage downloads.
### 1.1. Core Inputs
-**🔗 Creator/Post URL Input Field**
-- **Purpose**: Paste the URL of the content you want to download.
-- **Supported Sites**: Kemono.su, Coomer.party, Simpcity.su.
-- **Supported URL Types**:
- - Creator pages (e.g., `https://kemono.su/patreon/user/12345`).
- - Individual posts (e.g., `https://kemono.su/patreon/user/12345/post/98765`).
+**🔗 Creator/Post URL Input Field**
+- **Purpose**: Paste the URL of the content you want to download.
+- **Supported Sites**: Kemono.su, Coomer.party, Simpcity.su.
+- **Supported URL Types**:
+ - Creator pages (e.g., `https://kemono.su/patreon/user/12345`).
+ - Individual posts (e.g., `https://kemono.su/patreon/user/12345/post/98765`).
- **Note**: When ⭐ Favorite Mode is active, this field is disabled. For Simpcity.su URLs, the "Use Cookie" option is mandatory and auto-enabled.
-**🎨 Creator Selection Button**
-- **Icon**: 🎨 (Artist Palette)
-- **Purpose**: Opens the "Creator Selection" dialog to browse and queue downloads from known creators.
-- **Dialog Features**:
- - Loads creators from `creators.json`.
- - **Search Bar**: Filter creators by name.
- - **Creator List**: Displays creators with their service (e.g., Patreon, Fanbox).
- - **Selection**: Checkboxes to select one or more creators.
- - **Download Scope**: Organize downloads by Characters or Creators.
- - **Add to Queue**: Adds selected creators or their posts to the download queue.
+**🎨 Creator Selection Button**
+- **Icon**: 🎨 (Artist Palette)
+- **Purpose**: Opens the "Creator Selection" dialog to browse and queue downloads from known creators.
+- **Dialog Features**:
+ - Loads creators from `creators.json`.
+ - **Search Bar**: Filter creators by name.
+ - **Creator List**: Displays creators with their service (e.g., Patreon, Fanbox).
+ - **Selection**: Checkboxes to select one or more creators.
+ - **Download Scope**: Organize downloads by Characters or Creators.
+ - **Add to Queue**: Adds selected creators or their posts to the download queue.
-**Page Range (Start to End) Input Fields**
-- **Purpose**: Specify a range of pages to fetch for creator URLs.
-- **Usage**: Enter the starting and ending page numbers.
-- **Behavior**:
- - If blank, all pages are processed.
- - Disabled for single post URLs.
+**Page Range (Start to End) Input Fields**
+- **Purpose**: Specify a range of pages to fetch for creator URLs.
+- **Usage**: Enter the starting and ending page numbers.
+- **Behavior**:
+ - If blank, all pages are processed.
+ - Disabled for single post URLs.
-**📁 Download Location Input Field & Browse Button**
-- **Purpose**: Specify the main directory for downloaded files.
-- **Usage**: Type the path or click "Browse..." to select a folder.
+**📁 Download Location Input Field & Browse Button**
+- **Purpose**: Specify the main directory for downloaded files.
+- **Usage**: Type the path or click "Browse..." to select a folder.
- **Requirement**: Mandatory for all download operations.
### 1.2. Action Buttons
-**⬇️ Start Download / 🔗 Extract Links Button**
-- **Purpose**: Initiates downloading or link extraction.
-- **Behavior**:
- - Shows "🔗 Extract Links" if "Only Links" is selected.
- - Otherwise, shows "⬇️ Start Download".
- - Supports single-threaded or multi-threaded downloads based on settings.
+**⬇️ Start Download / 🔗 Extract Links Button**
+- **Purpose**: Initiates downloading or link extraction.
+- **Behavior**:
+ - Shows "🔗 Extract Links" if "Only Links" is selected.
+ - Otherwise, shows "⬇️ Start Download".
+ - Supports single-threaded or multi-threaded downloads based on settings.
-**🔄 Restore Download Button**
-- **Visibility**: Appears if an incomplete session is detected on startup.
+**🔄 Restore Download Button**
+- **Visibility**: Appears if an incomplete session is detected on startup.
- **Purpose**: Resumes a previously interrupted download session.
-**⏸️ Pause / ▶️ Resume Download Button**
-- **Purpose**: Pause or resume the ongoing download.
+**⏸️ Pause / ▶️ Resume Download Button**
+- **Purpose**: Pause or resume the ongoing download.
- **Behavior**: Toggles between "Pause" and "Resume". Some UI settings can be changed while paused.
-**❌ Cancel & Reset UI Button**
-- **Purpose**: Stops the current operation and performs a "soft" reset.
+**❌ Cancel & Reset UI Button**
+- **Purpose**: Stops the current operation and performs a "soft" reset.
- **Behavior**: Halts background threads, preserves URL and Download Location inputs, resets other settings.
-**🔄 Reset Button (in the log area)**
-- **Purpose**: Performs a "hard" reset when no operation is active.
+**🔄 Reset Button (in the log area)**
+- **Purpose**: Performs a "hard" reset when no operation is active.
- **Behavior**: Clears all inputs, resets options to default, and clears logs.
## 2. Filtering & Content Selection
These options allow precise control over downloaded content.
### 2.1. Content Filtering
-**🎯 Filter by Character(s) Input Field**
-- **Purpose**: Download content related to specific characters or series.
-- **Usage**: Enter comma-separated character names.
-- **Advanced Syntax**:
- - `Nami`: Simple filter.
- - `(Vivi, Ulti)`: Grouped filter. Matches posts with "Vivi" OR "Ulti". Creates a shared folder like `Vivi Ulti` if subfolders are enabled.
- - `(Boa, Hancock)~`: Aliased filter. Treats "Boa" and "Hancock" as the same entity.
+**🎯 Filter by Character(s) Input Field**
+- **Purpose**: Download content related to specific characters or series.
+- **Usage**: Enter comma-separated character names.
+- **Advanced Syntax**:
+ - `Nami`: Simple filter.
+ - `(Vivi, Ulti)`: Grouped filter. Matches posts with "Vivi" OR "Ulti". Creates a shared folder like `Vivi Ulti` if subfolders are enabled.
+ - `(Boa, Hancock)~`: Aliased filter. Treats "Boa" and "Hancock" as the same entity.
-**Filter: [Type] Button (Character Filter Scope)**
-- **Purpose**: Defines where the character filter is applied. Cycles on click.
-- **Options**:
- - **Filter: Title** (Default): Matches post titles.
- - **Filter: Files**: Matches filenames.
- - **Filter: Both**: Checks title first, then filenames.
- - **Filter: Comments (Beta)**: Checks filenames, then post comments.
+**Filter: [Type] Button (Character Filter Scope)**
+- **Purpose**: Defines where the character filter is applied. Cycles on click.
+- **Options**:
+ - **Filter: Title** (Default): Matches post titles.
+ - **Filter: Files**: Matches filenames.
+ - **Filter: Both**: Checks title first, then filenames.
+ - **Filter: Comments (Beta)**: Checks filenames, then post comments.
-**🚫 Skip with Words Input Field**
+**🚫 Skip with Words Input Field**
- **Purpose**: Exclude posts/files with specified keywords (e.g., `WIP`, `sketch`).
-**Scope: [Type] Button (Skip Words Scope)**
-- **Purpose**: Defines where skip words are applied. Cycles on click.
-- **Options**:
- - **Scope: Posts** (Default): Skips posts if the title contains a skip word.
- - **Scope: Files**: Skips files if the filename contains a skip word.
- - **Scope: Both**: Applies both rules.
+**Scope: [Type] Button (Skip Words Scope)**
+- **Purpose**: Defines where skip words are applied. Cycles on click.
+- **Options**:
+ - **Scope: Posts** (Default): Skips posts if the title contains a skip word.
+ - **Scope: Files**: Skips files if the filename contains a skip word.
+ - **Scope: Both**: Applies both rules.
-**✂️ Remove Words from Name Input Field**
+**✂️ Remove Words from Name Input Field**
- **Purpose**: Remove unwanted text from filenames (e.g., `patreon`, `[HD]`).
### 2.2. File Type Filtering
-**Filter Files (Radio Buttons)**
-- **Purpose**: Select file types to download.
-- **Options**:
- - **All**: All file types.
- - **Images/GIFs**: Common image formats.
- - **Videos**: Common video formats.
- - **🎧 Only Audio**: Common audio formats.
- - **📦 Only Archives**: Only `.zip` and `.rar` files.
- - **🔗 Only Links**: Extracts external links without downloading files.
+**Filter Files (Radio Buttons)**
+- **Purpose**: Select file types to download.
+- **Options**:
+ - **All**: All file types.
+ - **Images/GIFs**: Common image formats.
+ - **Videos**: Common video formats.
+ - **🎧 Only Audio**: Common audio formats.
+ - **📦 Only Archives**: Only `.zip` and `.rar` files.
+ - **🔗 Only Links**: Extracts external links without downloading files.
-**Skip .zip / Skip .rar Checkboxes**
-- **Purpose**: Skip downloading `.zip` or `.rar` files.
+**Skip .zip / Skip .rar Checkboxes**
+- **Purpose**: Skip downloading `.zip` or `.rar` files.
- **Behavior**: Disabled when "📦 Only Archives" is active.
## 3. Download Customization
Options to refine the download process and output.
-- **Download Thumbnails Only**: Downloads small preview images instead of full-resolution files.
-- **Scan Content for Images**: Scans post HTML for `
` tags, crucial for images in descriptions.
-- **Compress to WebP**: Converts images to WebP format (requires Pillow library).
+- **Download Thumbnails Only**: Downloads small preview images instead of full-resolution files.
+- **Scan Content for Images**: Scans post HTML for `
` tags, crucial for images in descriptions.
+- **Compress to WebP**: Converts images to WebP format (requires Pillow library).
+- **Keep Duplicates**: Normally, if a post contains multiple files with the same name, only the first is downloaded. Checking this option will download all of them, renaming subsequent unique files with a numeric suffix (e.g., `image_1.jpg`).
- **🗄️ Custom Folder Name (Single Post Only)**: Specify a custom folder name for a single post's content (appears if subfolders are enabled).
## 4. 📖 Manga/Comic Mode
A mode for downloading creator feeds in chronological order, ideal for sequential content.
-- **Activation**: Active when downloading a creator's entire feed (not a single post).
-- **Core Behavior**: Fetches all posts, processing from oldest to newest.
-- **Filename Style Toggle Button (in the log area)**:
- - **Purpose**: Controls file naming in Manga Mode. Cycles on click.
- - **Options**:
- - **Name: Post Title**: First file named after post title; others keep original names.
- - **Name: Original File**: Files keep server-provided names, with optional prefix.
- - **Name: Title+G.Num**: Global numbering with post title prefix (e.g., `Chapter 1_001.jpg`).
- - **Name: Date Based**: Sequential naming by post date (e.g., `001.jpg`), with optional prefix.
- - **Name: Post ID**: Files named after post ID to avoid clashes.
- - **Name: Date + Title**: Combines post date and title for filenames.
+- **Activation**: Active when downloading a creator's entire feed (not a single post).
+- **Core Behavior**: Fetches all posts, processing from oldest to newest.
+- **Filename Style Toggle Button (in the log area)**:
+ - **Purpose**: Controls file naming in Manga Mode. Cycles on click.
+ - **Options**:
+ - **Name: Post Title**: First file named after post title; others keep original names.
+ - **Name: Original File**: Files keep server-provided names, with optional prefix.
+ - **Name: Title+G.Num**: Global numbering with post title prefix (e.g., `Chapter 1_001.jpg`).
+ - **Name: Date Based**: Sequential naming by post date (e.g., `001.jpg`), with optional prefix.
+ - **Name: Post ID**: Files named after post ID to avoid clashes.
+ - **Name: Date + Title**: Combines post date and title for filenames.
## 5. Folder Organization & Known.txt
Controls for structuring downloaded content.
-- **Separate Folders by Name/Title Checkbox**: Enables automatic subfolder creation.
-- **Subfolder per Post Checkbox**: Creates subfolders for each post, named after the post title.
-- **Known.txt Management UI (Bottom Left)**:
- - **Purpose**: Manages a local `Known.txt` file for series, characters, or terms used in folder creation.
- - **List Display**: Shows primary names from `Known.txt`.
- - **➕ Add Button**: Adds names or groups (e.g., `(Character A, Alias B)~`).
- - **⤵️ Add to Filter Button**: Select names from `Known.txt` for the character filter.
- - **🗑️ Delete Selected Button**: Removes selected names from `Known.txt`.
- - **Open Known.txt Button**: Opens the file in the default text editor.
- - **❓ Help Button**: Opens this feature guide.
- - **📜 History Button**: Views recent download history.
+- **Separate Folders by Name/Title Checkbox**: Enables automatic subfolder creation.
+- **Subfolder per Post Checkbox**: Creates subfolders for each post, named after the post title.
+- **Date Prefix for Post Subfolders Checkbox**: When used with "Subfolder per Post," this option prefixes the folder name with the post's upload date (e.g., `2025-07-11 Post Title`), allowing for chronological sorting.
+- **Known.txt Management UI (Bottom Left)**:
+ - **Purpose**: Manages a local `Known.txt` file for series, characters, or terms used in folder creation.
+ - **List Display**: Shows primary names from `Known.txt`.
+ - **➕ Add Button**: Adds names or groups (e.g., `(Character A, Alias B)~`).
+ - **⤵️ Add to Filter Button**: Select names from `Known.txt` for the character filter.
+ - **🗑️ Delete Selected Button**: Removes selected names from `Known.txt`.
+ - **Open Known.txt Button**: Opens the file in the default text editor.
+ - **❓ Help Button**: Opens this feature guide.
+ - **📜 History Button**: Views recent download history.
## 6. ⭐ Favorite Mode (Kemono.su Only)
Download from favorited artists/posts on Kemono.su.
-- **Enable Checkbox ("⭐ Favorite Mode")**:
- - Switches to Favorite Mode.
- - Disables the main URL input.
- - Changes action buttons to "Favorite Artists" and "Favorite Posts".
- - Requires cookies.
-- **🖼️ Favorite Artists Button**: Select and download from favorited artists.
-- **📄 Favorite Posts Button**: Select and download specific favorited posts.
-- **Favorite Download Scope Button**:
- - **Scope: Selected Location**: Downloads favorites to the main directory.
- - **Scope: Artist Folders**: Creates subfolders per artist.
+- **Enable Checkbox ("⭐ Favorite Mode")**:
+ - Switches to Favorite Mode.
+ - Disables the main URL input.
+ - Changes action buttons to "Favorite Artists" and "Favorite Posts".
+ - Requires cookies.
+- **🖼️ Favorite Artists Button**: Select and download from favorited artists.
+- **📄 Favorite Posts Button**: Select and download specific favorited posts.
+- **Favorite Download Scope Button**:
+ - **Scope: Selected Location**: Downloads favorites to the main directory.
+ - **Scope: Artist Folders**: Creates subfolders per artist.
## 7. Advanced Settings & Performance
-- **🍪 Cookie Management**:
- - **Use Cookie Checkbox**: Enables cookies for restricted content.
- - **Cookie Text Field**: Paste cookie string.
- - **Browse... Button**: Select a `cookies.txt` file (Netscape format).
-- **Use Multithreading Checkbox & Threads Input**:
- - **Purpose**: Configures simultaneous operations.
- - **Behavior**: Sets concurrent post processing (creator feeds) or file downloads (single posts).
-- **Multi-part Download Toggle Button**:
- - **Purpose**: Enables/disables multi-segment downloading for large files.
- - **Note**: Best for large files; less efficient for small files.
+- **🍪 Cookie Management**:
+ - **Use Cookie Checkbox**: Enables cookies for restricted content.
+ - **Cookie Text Field**: Paste cookie string.
+ - **Browse... Button**: Select a `cookies.txt` file (Netscape format).
+- **Use Multithreading Checkbox & Threads Input**:
+ - **Purpose**: Configures simultaneous operations.
+ - **Behavior**: Sets concurrent post processing (creator feeds) or file downloads (single posts).
+- **Multi-part Download Toggle Button**:
+ - **Purpose**: Enables/disables multi-segment downloading for large files.
+ - **Note**: Best for large files; less efficient for small files.
## 8. Logging, Monitoring & Error Handling
-- **📜 Progress Log Area**: Displays messages, progress, and errors.
-- **👁️ / 🙈 Log View Toggle Button**: Switches between Progress Log and Missed Character Log (skipped posts).
-- **Show External Links in Log**: Displays external links (e.g., Mega, Google Drive) in a secondary panel.
-- **Export Links Button**: Saves extracted links to a `.txt` file in "Only Links" mode.
-- **Download Extracted Links Button**: Downloads files from supported external links in "Only Links" mode.
-- **🆘 Error Button & Dialog**:
- - **Purpose**: Active if files fail to download.
- - **Dialog Features**:
- - Lists failed files.
- - Retry failed downloads.
- - Export failed URLs to a text file.
+- **📜 Progress Log Area**: Displays messages, progress, and errors.
+- **👁️ / 🙈 Log View Toggle Button**: Switches between Progress Log and Missed Character Log (skipped posts).
+- **Show External Links in Log**: Displays external links (e.g., Mega, Google Drive) in a secondary panel.
+- **Export Links Button**: Saves extracted links to a `.txt` file in "Only Links" mode.
+- **Download Extracted Links Button**: Downloads files from supported external links in "Only Links" mode.
+- **🆘 Error Button & Dialog**:
+ - **Purpose**: Active if files fail to download. The button will display a live count of failed files (e.g., **(3) Error**).
+ - **Dialog Features**:
+ - Lists failed files.
+ - Retry failed downloads.
+ - Export failed URLs to a text file.
## 9. Application Settings (⚙️)
-- **Appearance**: Switch between Light and Dark themes.
-- **Language**: Change UI language (restart required).
+- **Appearance**: Switch between Light and Dark themes.
+- **Language**: Change UI language (restart required).
\ No newline at end of file
diff --git a/readme.md b/readme.md
index a6bbb75..31e3f82 100644
--- a/readme.md
+++ b/readme.md
@@ -1,45 +1,42 @@
-
Kemono Downloader v5.6.0
+Kemono Downloader v6.0.0
-
-
- 
- Default
- |
-
- 
- Favorite mode
- |
-
-
-
- 
- Single Post
- |
-
- 
- Manga/Comic Mode
- |
-
+
+
+ 
+ Default
+ |
+
+ 
+ Favorite mode
+ |
+
+
+
+ 
+ Single Post
+ |
+
+ 
+ Manga/Comic Mode
+ |
+
---
A powerful, feature-rich GUI application for downloading content from **[Kemono.su](https://kemono.su)** (and its mirrors like kemono.party) and **[Coomer.party](https://coomer.party)** (and its mirrors like coomer.su).
Built with PyQt5, this tool is designed for users who want deep filtering capabilities, customizable folder structures, efficient downloads, and intelligent automation, all within a modern and user-friendly graphical interface.
-*This v5.0.0 release marks a significant feature milestone. Future updates are expected to be less frequent, focusing on maintenance and minor refinements.*
-*Update v5.2.0 introduces multi-language support, theme selection, and further UI refinements.*
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
---
@@ -48,74 +45,81 @@ Built with PyQt5, this tool is designed for users who want deep filtering capabi
Kemono Downloader offers a range of features to streamline your content downloading experience:
-- **User-Friendly Interface:** A modern PyQt5 GUI for easy navigation and operation.
-- **Flexible Downloading:**
- - Download content from Kemono.su (and mirrors) and Coomer.party (and mirrors).
- - Supports creator pages (with page range selection) and individual post URLs.
- - Standard download controls: Start, Pause, Resume, and Cancel.
-- **Powerful Filtering:**
- - **Character Filtering:** Filter content by character names. Supports simple comma-separated names and grouped names for shared folders.
- - **Keyword Skipping:** Skip posts or files based on specified keywords.
- - **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 `
` 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.
+- **User-Friendly Interface:** A modern PyQt5 GUI for easy navigation and operation.
+- **Flexible Downloading:**
+ - Download content from Kemono.su (and mirrors) and Coomer.party (and mirrors).
+ - Supports creator pages (with page range selection) and individual post URLs.
+ - Standard download controls: Start, Pause, Resume, and Cancel.
+- **Powerful Filtering:**
+ - **Character Filtering:** Filter content by character names. Supports simple comma-separated names and grouped names for shared folders.
+ - **Keyword Skipping:** Skip posts or files based on specified keywords.
+ - **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 `
` 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 v5.3.0
-- **Multi-Creator Post Fetching & Queuing:**
- - The **Creator Selection popup** (🎨 icon) has been significantly enhanced.
- - After selecting multiple creators, you can now click a new "**Fetch Posts**" button.
- - This will retrieve and display posts from all selected creators in a new view within the popup.
- - You can then browse these fetched posts (with search functionality) and select individual posts.
- - A new "**Add Selected Posts to Queue**" button allows you to add your chosen posts directly to the main download queue, streamlining the process of gathering content from multiple artists.
- - The traditional "**Add Selected to URL**" button is still available if you prefer to populate the main URL field with creator names.
-- **Improved Favorite Download Queue Handling:**
- - When items are added to the download queue from the Creator Selection popup, the main URL input field will now display a placeholder message (e.g., "{count} items in queue from popup").
- - The queue is now more robustly managed, especially when interacting with the main URL input field after items have been queued from the popup.
+## ✨ 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 instantly shows you how many files have failed during a download. It will update with a live count, like **`(3) Error`**, so you can see the status without opening the dialog.
+- **Date Prefix for Post Subfolders**: A new **"Date Prefix"** checkbox is available in the advanced settings. When used with the "Subfolder per Post" option, it automatically adds the post's upload date to the folder name (e.g., `2025-07-11 Post Title`), making it easy to sort your downloads chronologically.
+- **Keep Duplicates Within a Post**: A **"Keep Duplicates"** checkbox has been added. This feature tells the downloader to save all files from a single post, even if they have the same name. Unique files will be saved with a numbered suffix (like `image_1.jpg`) instead of being skipped.
+
+### Bug Fixes & Quality of Life
+- The downloader is now more resilient and correctly renames large `.part` files upon completion.
+- The list of failed files in the Error Dialog is now correctly saved and restored with your session.
+- Your selected download location will now be remembered even after clicking "Reset".
+- The "Cancel" button is now enabled when a download is pending restoration, allowing you to discard the session easily.
+- Internal cleanup messages have been removed from the final download log for a cleaner summary.
---
-## ✨ What's New in v5.1.0
-- **Enhanced Error File Management**: The "Error" button now opens a dialog listing files that failed to download. This dialog includes:
- - An option to **retry selected** failed downloads.
- - A new **"Export URLs to .txt"** button, allowing users to save links of failed downloads either as "URL only" or "URL with details" (including post title, ID, and original filename).
- - Fixed a bug where files skipped during retry (due to existing hash match) were not correctly removed from the error list.
-- **Improved UI Stability**: Addressed issues with UI state management to more accurately reflect ongoing download activities (including retries and external link downloads). This prevents the "Cancel" button from becoming inactive prematurely while operations are still running.
+## Next Update Plans
-## ✨ What's New in v5.2.0
-- **Multi-language Support:** The interface now supports multiple languages: English, Japanese, French, Spanish, German, Russian, Korean, and Chinese (Simplified). Select your preferred language in the new Settings dialog.
-- **Theme Selection:** Choose between Light and Dark application themes via the Settings dialog for a personalized viewing experience.
-- **Centralized Settings:** A new Settings dialog (accessible via a settings button, often with a gear icon) provides a dedicated space for language and appearance customizations.
-- **Internal Localization:** Introduced `languages.py` for managing UI translations, streamlining the addition of new languages by contributors.
+### 🔖 Post Tag Filtering (Planned for v6.1.0)
+A new **"Filter by Post Tags"** feature will allow users to:
+- Filter and download content based on specific post tags
+- Combine tag filtering with existing character filters and file types.
+- Use tag presets to streamline repetitive downloads.
----
+This will give **greater control and precision** when downloading content from creators who use organized tagging.
+
+### 📁 Creator Download History (.json Save)
+To make incremental downloads easier, a new feature will let you:
+- **Save a `.json` file** with information about the posts you've already downloaded from a creator.
+- On future runs, the downloader will **compare this file to the live site** and download only **new posts**.
+- Great for keeping collections up-to-date without redownloading old content.
+
+This will be especially useful for users who regularly sync with creators and want to avoid duplicates or skipped posts.
## Installation
### Requirements
-- Python 3.6 or higher
-- pip (Python package installer)
+- Python 3.6 or higher
+- pip (Python package installer)
### Install Dependencies
Open your terminal or command prompt and run:
diff --git a/src/core/workers.py b/src/core/workers.py
index 377c79e..0f2ba1e 100644
--- a/src/core/workers.py
+++ b/src/core/workers.py
@@ -78,6 +78,7 @@ class PostProcessorWorker:
creator_download_folder_ignore_words =None ,
manga_global_file_counter_ref =None ,
use_date_prefix_for_subfolder=False,
+ keep_in_post_duplicates=False,
session_file_path=None,
session_lock=None,
):
@@ -130,6 +131,7 @@ class PostProcessorWorker:
self .scan_content_for_images =scan_content_for_images
self .creator_download_folder_ignore_words =creator_download_folder_ignore_words
self.use_date_prefix_for_subfolder = use_date_prefix_for_subfolder
+ self.keep_in_post_duplicates = keep_in_post_duplicates
self.session_file_path = session_file_path
self.session_lock = session_lock
if self .compress_images and Image is None :
@@ -555,58 +557,175 @@ class PostProcessorWorker:
final_total_for_progress =total_size_bytes if download_successful_flag and total_size_bytes >0 else downloaded_size_bytes
self ._emit_signal ('file_progress',api_original_filename ,(downloaded_size_bytes ,final_total_for_progress ))
- if self .check_cancel ()or (skip_event and skip_event .is_set ())or (self .pause_event and self .pause_event .is_set ()and not download_successful_flag ):
- self .logger (f" ⚠️ Download process interrupted for {api_original_filename }.")
- if downloaded_part_file_path and os .path .exists (downloaded_part_file_path ):
- try :os .remove (downloaded_part_file_path )
- except OSError :pass
- return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_SKIPPED ,None
+ # Rescue download if an IncompleteRead error occurred but the file is complete
+ if (not download_successful_flag and
+ isinstance(last_exception_for_retry_later, http.client.IncompleteRead) and
+ total_size_bytes > 0 and downloaded_part_file_path and os.path.exists(downloaded_part_file_path)):
+ try:
+ actual_size = os.path.getsize(downloaded_part_file_path)
+ if actual_size == total_size_bytes:
+ self.logger(f" ✅ Rescued '{api_original_filename}': IncompleteRead error occurred, but file size matches. Proceeding with save.")
+ download_successful_flag = True
+ # The hash must be recalculated now that we've verified the file
+ md5_hasher = hashlib.md5()
+ with open(downloaded_part_file_path, 'rb') as f_verify:
+ for chunk in iter(lambda: f_verify.read(8192), b""): # Read in chunks
+ md5_hasher.update(chunk)
+ calculated_file_hash = md5_hasher.hexdigest()
+ except Exception as rescue_exc:
+ self.logger(f" ⚠️ Failed to rescue file despite matching size. Error: {rescue_exc}")
- if not download_successful_flag :
- self .logger (f"❌ Download failed for '{api_original_filename }' after {max_retries +1 } attempts.")
+ if self.check_cancel() or (skip_event and skip_event.is_set()) or (self.pause_event and self.pause_event.is_set() and not download_successful_flag):
+ self.logger(f" ⚠️ Download process interrupted for {api_original_filename}.")
+ if downloaded_part_file_path and os.path.exists(downloaded_part_file_path):
+ try: os.remove(downloaded_part_file_path)
+ except OSError: pass
+ return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
+ # This logic block now correctly handles all outcomes: success, failure, or rescued.
+ if download_successful_flag:
+ # --- This is the success path ---
+ if self._check_pause(f"Post-download hash check for '{api_original_filename}'"):
+ return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
- is_actually_incomplete_read =False
- if isinstance (last_exception_for_retry_later ,http .client .IncompleteRead ):
- is_actually_incomplete_read =True
- elif hasattr (last_exception_for_retry_later ,'__cause__')and isinstance (last_exception_for_retry_later .__cause__ ,http .client .IncompleteRead ):
- is_actually_incomplete_read =True
+ with self.downloaded_file_hashes_lock:
+ if calculated_file_hash in self.downloaded_file_hashes:
+ self.logger(f" -> Skip Saving Duplicate (Hash Match): '{api_original_filename}' (Hash: {calculated_file_hash[:8]}...).")
+ with self.downloaded_files_lock: self.downloaded_files.add(filename_to_save_in_main_path)
+ if downloaded_part_file_path and os.path.exists(downloaded_part_file_path):
+ try: os.remove(downloaded_part_file_path)
+ except OSError as e_rem: self.logger(f" -> Failed to remove .part file for hash duplicate: {e_rem}")
+ return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
- elif last_exception_for_retry_later is not None :
- str_exc =str (last_exception_for_retry_later ).lower ()
+ effective_save_folder = target_folder_path
+ filename_after_styling_and_word_removal = filename_to_save_in_main_path
- if "incompleteread"in str_exc or (isinstance (last_exception_for_retry_later ,tuple )and any ("incompleteread"in str (arg ).lower ()for arg in last_exception_for_retry_later if isinstance (arg ,(str ,Exception )))):
- is_actually_incomplete_read =True
+ try:
+ os.makedirs(effective_save_folder, exist_ok=True)
+ except OSError as e:
+ self.logger(f" ❌ Critical error creating directory '{effective_save_folder}': {e}. Skipping file '{api_original_filename}'.")
+ if downloaded_part_file_path and os.path.exists(downloaded_part_file_path):
+ try: os.remove(downloaded_part_file_path)
+ except OSError: pass
+ return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
- if is_actually_incomplete_read :
- self .logger (f" Marking '{api_original_filename }' for potential retry later due to IncompleteRead.")
- retry_later_details ={
- 'file_info':file_info ,
- 'target_folder_path':target_folder_path ,
- 'headers':headers ,
- 'original_post_id_for_log':original_post_id_for_log ,
- 'post_title':post_title ,
- 'file_index_in_post':file_index_in_post ,
- 'num_files_in_this_post':num_files_in_this_post ,
- 'forced_filename_override':filename_to_save_in_main_path ,
- 'manga_mode_active_for_file':self .manga_mode_active ,
- 'manga_filename_style_for_file':self .manga_filename_style ,
+ data_to_write_io = None
+ filename_after_compression = filename_after_styling_and_word_removal
+ is_img_for_compress_check = is_image(api_original_filename)
+
+ if is_img_for_compress_check and self.compress_images and Image and downloaded_size_bytes > (1.5 * 1024 * 1024):
+ # ... (This block for image compression remains the same)
+ self .logger (f" Compressing '{api_original_filename }' ({downloaded_size_bytes /(1024 *1024 ):.2f} MB)...")
+ if self ._check_pause (f"Image compression for '{api_original_filename }'"):return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_SKIPPED ,None
+ img_content_for_pillow =None
+ try :
+ with open (downloaded_part_file_path ,'rb')as f_img_in :
+ img_content_for_pillow =BytesIO (f_img_in .read ())
+ with Image .open (img_content_for_pillow )as img_obj :
+ if img_obj .mode =='P':img_obj =img_obj .convert ('RGBA')
+ elif img_obj .mode not in ['RGB','RGBA','L']:img_obj =img_obj .convert ('RGB')
+ compressed_output_io =BytesIO ()
+ img_obj .save (compressed_output_io ,format ='WebP',quality =80 ,method =4 )
+ compressed_size =compressed_output_io .getbuffer ().nbytes
+ if compressed_size Failed to remove .part after compression: {e_rem}")
+ else:
+ if downloaded_part_file_path and os.path.exists(downloaded_part_file_path):
+ time.sleep(0.1)
+ os.rename(downloaded_part_file_path, final_save_path)
+ else:
+ raise FileNotFoundError(f"Original .part file not found for saving: {downloaded_part_file_path}")
+
+ with self.downloaded_file_hashes_lock: self.downloaded_file_hashes.add(calculated_file_hash)
+ with self.downloaded_files_lock: self.downloaded_files.add(filename_to_save_in_main_path)
+
+ final_filename_saved_for_return = final_filename_on_disk
+ self.logger(f"✅ Saved: '{final_filename_saved_for_return}' (from '{api_original_filename}', {downloaded_size_bytes / (1024 * 1024):.2f} MB) in '{os.path.basename(effective_save_folder)}'")
+
+ downloaded_file_details = {
+ 'disk_filename': final_filename_saved_for_return,
+ 'post_title': post_title,
+ 'post_id': original_post_id_for_log,
+ 'upload_date_str': self.post.get('published') or self.post.get('added') or "N/A",
+ 'download_timestamp': time.time(),
+ 'download_path': effective_save_folder,
+ 'service': self.service,
+ 'user_id': self.user_id,
+ 'api_original_filename': api_original_filename,
+ 'folder_context_name': folder_context_name_for_history or os.path.basename(effective_save_folder)
}
- return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER ,retry_later_details
- else :
- self .logger (f" Marking '{api_original_filename }' as permanently failed for this session.")
- permanent_failure_details ={
- 'file_info':file_info ,
- 'target_folder_path':target_folder_path ,
- 'headers':headers ,
- 'original_post_id_for_log':original_post_id_for_log ,
- 'post_title':post_title ,
- 'file_index_in_post':file_index_in_post ,
- 'num_files_in_this_post':num_files_in_this_post ,
- 'forced_filename_override':filename_to_save_in_main_path ,
- }
- return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_FAILED_PERMANENTLY_THIS_SESSION ,permanent_failure_details
- if self ._check_pause (f"Post-download hash check for '{api_original_filename }'"):return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_SKIPPED ,None
+ self._emit_signal('file_successfully_downloaded', downloaded_file_details)
+ time.sleep(0.05)
+
+ return 1, 0, final_filename_saved_for_return, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SUCCESS, None
+ except Exception as save_err:
+ self.logger(f"->>Save Fail for '{final_filename_on_disk}': {save_err}")
+ if os.path.exists(final_save_path):
+ try: os.remove(final_save_path)
+ except OSError: self.logger(f" -> Failed to remove partially saved file: {final_save_path}")
+ return 0, 1, final_filename_saved_for_return, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
+ finally:
+ if data_to_write_io and hasattr(data_to_write_io, 'close'):
+ data_to_write_io.close()
+
+ else:
+ # --- This is the failure path ---
+ self.logger(f"❌ Download failed for '{api_original_filename}' after {max_retries + 1} attempts.")
+
+ is_actually_incomplete_read = False
+ if isinstance(last_exception_for_retry_later, http.client.IncompleteRead):
+ is_actually_incomplete_read = True
+ elif hasattr(last_exception_for_retry_later, '__cause__') and isinstance(last_exception_for_retry_later.__cause__, http.client.IncompleteRead):
+ is_actually_incomplete_read = True
+ elif last_exception_for_retry_later is not None:
+ str_exc = str(last_exception_for_retry_later).lower()
+ if "incompleteread" in str_exc or (isinstance(last_exception_for_retry_later, tuple) and any("incompleteread" in str(arg).lower() for arg in last_exception_for_retry_later if isinstance(arg, (str, Exception)))):
+ is_actually_incomplete_read = True
+
+ if is_actually_incomplete_read:
+ self.logger(f" Marking '{api_original_filename}' for potential retry later due to IncompleteRead.")
+ retry_later_details = { 'file_info': file_info, 'target_folder_path': target_folder_path, 'headers': headers, 'original_post_id_for_log': original_post_id_for_log, 'post_title': post_title, 'file_index_in_post': file_index_in_post, 'num_files_in_this_post': num_files_in_this_post, 'forced_filename_override': filename_to_save_in_main_path, 'manga_mode_active_for_file': self.manga_mode_active, 'manga_filename_style_for_file': self.manga_filename_style, }
+ return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER, retry_later_details
+ else:
+ self.logger(f" Marking '{api_original_filename}' as permanently failed for this session.")
+ permanent_failure_details = { 'file_info': file_info, 'target_folder_path': target_folder_path, 'headers': headers, 'original_post_id_for_log': original_post_id_for_log, 'post_title': post_title, 'file_index_in_post': file_index_in_post, 'num_files_in_this_post': num_files_in_this_post, 'forced_filename_override': filename_to_save_in_main_path, }
+ return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_FAILED_PERMANENTLY_THIS_SESSION, permanent_failure_details
with self .downloaded_file_hashes_lock :
if calculated_file_hash in self .downloaded_file_hashes :
self .logger (f" -> Skip Saving Duplicate (Hash Match): '{api_original_filename }' (Hash: {calculated_file_hash [:8 ]}...).")
@@ -1207,16 +1326,26 @@ class PostProcessorWorker:
return 0 ,0 ,[],[],[],None
files_to_download_info_list =[]
processed_original_filenames_in_this_post =set ()
- for file_info in all_files_from_post_api :
- current_api_original_filename =file_info .get ('_original_name_for_log')
- if current_api_original_filename in processed_original_filenames_in_this_post :
- self .logger (f" -> Skip Duplicate Original Name (within post {post_id }): '{current_api_original_filename }' already processed/listed for this post.")
- total_skipped_this_post +=1
- else :
- files_to_download_info_list .append (file_info )
- if current_api_original_filename :
- processed_original_filenames_in_this_post .add (current_api_original_filename )
- if not files_to_download_info_list :
+
+ if self.keep_in_post_duplicates:
+ # If we keep duplicates, just add every file to the list to be processed.
+ # The downstream hash check and rename-on-collision logic will handle them.
+ files_to_download_info_list.extend(all_files_from_post_api)
+ self.logger(f" ℹ️ 'Keep Duplicates' is on. All {len(all_files_from_post_api)} files from post will be processed.")
+ else:
+ # This is the original logic that skips duplicates by name within a post.
+ for file_info in all_files_from_post_api:
+ current_api_original_filename = file_info.get('_original_name_for_log')
+ if current_api_original_filename in processed_original_filenames_in_this_post:
+ self.logger(f" -> Skip Duplicate Original Name (within post {post_id}): '{current_api_original_filename}' already processed/listed for this post.")
+ total_skipped_this_post += 1
+ else:
+ files_to_download_info_list.append(file_info)
+ if current_api_original_filename:
+ processed_original_filenames_in_this_post.add(current_api_original_filename)
+
+ if not files_to_download_info_list:
+
self .logger (f" All files for post {post_id } were duplicate original names or skipped earlier.")
return 0 ,total_skipped_this_post ,[],[],[],None
@@ -1366,12 +1495,24 @@ class PostProcessorWorker:
with open(self.session_file_path, 'r', encoding='utf-8') as f:
session_data = json.load(f)
- # Modify in memory
- if not isinstance(session_data.get('download_state', {}).get('processed_post_ids'), list):
- if 'download_state' not in session_data:
- session_data['download_state'] = {}
+ if 'download_state' not in session_data:
+ session_data['download_state'] = {}
+
+ # Add processed ID
+ if not isinstance(session_data['download_state'].get('processed_post_ids'), list):
session_data['download_state']['processed_post_ids'] = []
session_data['download_state']['processed_post_ids'].append(self.post.get('id'))
+
+ # Add any permanent failures from this worker to the session file
+ if permanent_failures_this_post:
+ if not isinstance(session_data['download_state'].get('permanently_failed_files'), list):
+ session_data['download_state']['permanently_failed_files'] = []
+ # To avoid duplicates if the same post is somehow re-processed
+ existing_failed_urls = {f.get('file_info', {}).get('url') for f in session_data['download_state']['permanently_failed_files']}
+ for failure in permanent_failures_this_post:
+ if failure.get('file_info', {}).get('url') not in existing_failed_urls:
+ session_data['download_state']['permanently_failed_files'].append(failure)
+
# Write to temp file and then atomically replace
temp_file_path = self.session_file_path + ".tmp"
with open(temp_file_path, 'w', encoding='utf-8') as f_tmp:
@@ -1460,6 +1601,7 @@ class DownloadThread (QThread ):
scan_content_for_images =False ,
creator_download_folder_ignore_words =None ,
use_date_prefix_for_subfolder=False,
+ keep_in_post_duplicates=False,
cookie_text ="",
session_file_path=None,
session_lock=None,
@@ -1513,6 +1655,7 @@ class DownloadThread (QThread ):
self .scan_content_for_images =scan_content_for_images
self .creator_download_folder_ignore_words =creator_download_folder_ignore_words
self.use_date_prefix_for_subfolder = use_date_prefix_for_subfolder
+ self.keep_in_post_duplicates = keep_in_post_duplicates
self .manga_global_file_counter_ref =manga_global_file_counter_ref
self.session_file_path = session_file_path
self.session_lock = session_lock
@@ -1646,6 +1789,7 @@ class DownloadThread (QThread ):
use_cookie =self .use_cookie ,
manga_date_file_counter_ref =self .manga_date_file_counter_ref ,
use_date_prefix_for_subfolder=self.use_date_prefix_for_subfolder,
+ keep_in_post_duplicates=self.keep_in_post_duplicates,
creator_download_folder_ignore_words =self .creator_download_folder_ignore_words ,
session_file_path=self.session_file_path,
session_lock=self.session_lock,
diff --git a/src/ui/dialogs/HelpGuideDialog.py b/src/ui/dialogs/HelpGuideDialog.py
index 9b721cd..8c9a634 100644
--- a/src/ui/dialogs/HelpGuideDialog.py
+++ b/src/ui/dialogs/HelpGuideDialog.py
@@ -4,7 +4,7 @@ import sys
# --- PyQt5 Imports ---
from PyQt5.QtCore import QUrl, QSize, Qt
-from PyQt5.QtGui import QIcon
+from PyQt5.QtGui import QIcon, QDesktopServices
from PyQt5.QtWidgets import (
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
QStackedWidget, QScrollArea, QFrame, QWidget
diff --git a/src/ui/main_window.py b/src/ui/main_window.py
index 46c9911..bec9096 100644
--- a/src/ui/main_window.py
+++ b/src/ui/main_window.py
@@ -241,7 +241,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 v5.5.0")
+ self.setWindowTitle("Kemono Downloader v6.0.0")
self.init_ui()
self._connect_signals()
self.log_signal.emit("ℹ️ Local API server functionality has been removed.")
@@ -274,6 +274,8 @@ class DownloaderApp (QWidget ):
'use_subfolder_per_post_checkbox': 'use_post_subfolders',
'use_multithreading_checkbox': 'use_multithreading',
'external_links_checkbox': 'show_external_links',
+ 'keep_duplicates_checkbox': 'keep_in_post_duplicates',
+ 'date_prefix_checkbox': 'use_date_prefix_for_subfolder',
'manga_mode_checkbox': 'manga_mode_active',
'scan_content_images_checkbox': 'scan_content_for_images',
'use_cookie_checkbox': 'use_cookie',
@@ -342,6 +344,12 @@ class DownloaderApp (QWidget ):
if "ui_settings" not in session_data or "download_state" not in session_data:
raise ValueError("Invalid session file structure.")
+ failed_files_from_session = session_data.get('download_state', {}).get('permanently_failed_files', [])
+ if failed_files_from_session:
+ self.permanently_failed_files_for_dialog.clear()
+ self.permanently_failed_files_for_dialog.extend(failed_files_from_session)
+ self.log_signal.emit(f"ℹ️ Restored {len(failed_files_from_session)} failed file entries from the previous session.")
+
self.interrupted_session_data = session_data
self.log_signal.emit("ℹ️ Incomplete download session found. UI updated for restore.")
self._prepare_ui_for_restore()
@@ -422,11 +430,13 @@ class DownloaderApp (QWidget ):
self.pause_btn.setEnabled(True)
self.pause_btn.clicked.connect(self.restore_download)
self.pause_btn.setToolTip(self._tr("restore_download_button_tooltip", "Click to restore the interrupted download."))
- self.cancel_btn.setEnabled(True)
- self.cancel_btn.setText(self._tr("cancel_button_text", "❌ Cancel & Reset UI"))
- self.cancel_btn.setEnabled(False) # Nothing to cancel yet
- self.cancel_btn.setToolTip(self._tr("cancel_button_tooltip", "Click to cancel the ongoing download/extraction process and reset the UI fields (preserving URL and Directory)."))
+ # --- START: CORRECTED CANCEL BUTTON LOGIC ---
+ self.cancel_btn.setText(self._tr("discard_session_button_text", "🗑️ Discard Session"))
+ self.cancel_btn.setEnabled(True)
+ self.cancel_btn.clicked.connect(self._clear_session_and_reset_ui)
+ self.cancel_btn.setToolTip(self._tr("discard_session_tooltip", "Click to discard the interrupted session and reset the UI."))
+
elif is_download_active:
# State: Downloading / Paused
self.download_btn.setText(self._tr("start_download_button_text", "⬇️ Start Download"))
@@ -1206,6 +1216,11 @@ class DownloaderApp (QWidget ):
self .compress_images_checkbox .setToolTip ("Compress images > 1.5MB to WebP format (requires Pillow).")
row1_layout .addWidget (self .compress_images_checkbox )
+ self.keep_duplicates_checkbox = QCheckBox("Keep Duplicates")
+ self.keep_duplicates_checkbox.setChecked(False)
+ self.keep_duplicates_checkbox.setToolTip("If checked, downloads all files from a post even if they have the same name.\nUnique files will be renamed with a suffix; identical files will still be skipped by hash.")
+ row1_layout.addWidget(self.keep_duplicates_checkbox)
+
row1_layout .addStretch (1 )
checkboxes_group_layout .addLayout (row1_layout )
@@ -3578,6 +3593,7 @@ class DownloaderApp (QWidget ):
self .retryable_failed_files_info .clear ()
self .permanently_failed_files_for_dialog .clear ()
+ self._update_error_button_count()
manga_date_file_counter_ref_for_thread =None
if manga_mode and self .manga_filename_style ==STYLE_DATE_BASED and not extract_links_only :
@@ -3627,6 +3643,9 @@ class DownloaderApp (QWidget ):
if not extract_links_only :
log_messages .append (f" Subfolders: {'Enabled'if use_subfolders else 'Disabled'}")
+ if use_subfolders and self.use_subfolder_per_post_checkbox.isChecked():
+ use_date_prefix = self.date_prefix_checkbox.isChecked() if hasattr(self, 'date_prefix_checkbox') else False
+ log_messages.append(f" ↳ Date Prefix for Post Subfolders: {'Enabled' if use_date_prefix else 'Disabled'}")
if use_subfolders :
if custom_folder_name_cleaned :log_messages .append (f" Custom Folder (Post): '{custom_folder_name_cleaned }'")
if actual_filters_to_use_for_run :
@@ -3636,14 +3655,15 @@ class DownloaderApp (QWidget ):
log_messages .append (f" Folder Naming: Automatic (based on title/known names)")
- log_messages .extend ([
- f" File Type Filter: {user_selected_filter_text } (Backend processing as: {backend_filter_mode })",
- f" Skip Archives: {'.zip'if effective_skip_zip else ''}{', 'if effective_skip_zip and effective_skip_rar else ''}{'.rar'if effective_skip_rar else ''}{'None (Archive Mode)'if backend_filter_mode =='archive'else ('None'if not (effective_skip_zip or effective_skip_rar )else '')}",
- f" Skip Words (posts/files): {', '.join (skip_words_list )if skip_words_list else 'None'}",
- f" Skip Words Scope: {current_skip_words_scope .capitalize ()}",
- f" Remove Words from Filename: {', '.join (remove_from_filename_words_list )if remove_from_filename_words_list else 'None'}",
- f" Compress Images: {'Enabled'if compress_images else 'Disabled'}",
- f" Thumbnails Only: {'Enabled'if download_thumbnails else 'Disabled'}"
+ keep_duplicates = self.keep_duplicates_checkbox.isChecked() if hasattr(self, 'keep_duplicates_checkbox') else False
+ log_messages.extend([
+ f" File Type Filter: {user_selected_filter_text} (Backend processing as: {backend_filter_mode})",
+ f" Keep In-Post Duplicates: {'Enabled' if keep_duplicates else 'Disabled'}",
+ f" Skip Archives: {'.zip' if effective_skip_zip else ''}{', ' if effective_skip_zip and effective_skip_rar else ''}{'.rar' if effective_skip_rar else ''}{'None (Archive Mode)' if backend_filter_mode == 'archive' else ('None' if not (effective_skip_zip or effective_skip_rar) else '')}",
+ f" Skip Words Scope: {current_skip_words_scope .capitalize ()}",
+ f" Remove Words from Filename: {', '.join (remove_from_filename_words_list )if remove_from_filename_words_list else 'None'}",
+ f" Compress Images: {'Enabled'if compress_images else 'Disabled'}",
+ f" Thumbnails Only: {'Enabled'if download_thumbnails else 'Disabled'}"
])
log_messages .append (f" Scan Post Content for Images: {'Enabled'if scan_content_for_images else 'Disabled'}")
else :
@@ -3731,6 +3751,7 @@ class DownloaderApp (QWidget ):
'session_lock': self.session_lock,
'creator_download_folder_ignore_words':creator_folder_ignore_words_for_run ,
'use_date_prefix_for_subfolder': self.date_prefix_checkbox.isChecked() if hasattr(self, 'date_prefix_checkbox') else False,
+ 'keep_in_post_duplicates': self.keep_duplicates_checkbox.isChecked() if hasattr(self, 'keep_duplicates_checkbox') else False,
}
args_template ['override_output_dir']=override_output_dir
@@ -3830,6 +3851,7 @@ class DownloaderApp (QWidget ):
dialog .exec_ ()
def _handle_retry_from_error_dialog (self ,selected_files_to_retry ):
self ._start_failed_files_retry_session (files_to_retry_list =selected_files_to_retry )
+ self._update_error_button_count()
def _handle_retryable_file_failure (self ,list_of_retry_details ):
"""Appends details of files that failed but might be retryable later."""
@@ -3841,6 +3863,7 @@ class DownloaderApp (QWidget ):
if list_of_permanent_failure_details :
self .permanently_failed_files_for_dialog .extend (list_of_permanent_failure_details )
self .log_signal .emit (f"ℹ️ {len (list_of_permanent_failure_details )} file(s) from single-thread download marked as permanently failed for this session.")
+ self._update_error_button_count()
def _submit_post_to_worker_pool (self ,post_data_item ,worker_args_template ,num_file_dl_threads_for_each_worker ,emitter_for_worker ,ppw_expected_keys ,ppw_optional_keys_with_defaults ):
"""Helper to prepare and submit a single post processing task to the thread pool."""
@@ -4129,6 +4152,7 @@ class DownloaderApp (QWidget ):
'manga_filename_style',
'manga_date_prefix',
'use_date_prefix_for_subfolder',
+ 'keep_in_post_duplicates',
'manga_global_file_counter_ref',
'creator_download_folder_ignore_words',
'session_file_path',
@@ -4242,6 +4266,7 @@ class DownloaderApp (QWidget ):
self ._add_to_history_candidates (history_data_from_worker )
if permanent_failures_from_post :
self .permanently_failed_files_for_dialog .extend (permanent_failures_from_post )
+ self._update_error_button_count()
self ._add_to_history_candidates (history_data_from_worker )
with self .downloaded_files_lock :
self .download_counter +=downloaded_files_from_future
@@ -4955,6 +4980,7 @@ class DownloaderApp (QWidget ):
# --- Reset UI and all state ---
self.log_signal.emit("🔄 Resetting application state to defaults...")
self._reset_ui_to_defaults()
+ self._load_saved_download_location()
self.main_log_output.clear()
self.external_log_output.clear()
if self.missed_character_log_output:
@@ -4989,6 +5015,7 @@ class DownloaderApp (QWidget ):
self.only_links_log_display_mode = LOG_DISPLAY_LINKS
self.mega_download_log_preserved_once = False
self.permanently_failed_files_for_dialog.clear()
+ self._update_error_button_count()
self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION
self._update_favorite_scope_button_text()
self.retryable_failed_files_info.clear()
@@ -5024,7 +5051,6 @@ class DownloaderApp (QWidget ):
"""Resets all UI elements and relevant state to their default values."""
# Clear all text fields
self.link_input.clear()
- self.dir_input.clear()
self.custom_folder_input.clear()
self.character_input.clear()
self.skip_words_input.clear()
@@ -5207,6 +5233,19 @@ class DownloaderApp (QWidget ):
self .multipart_toggle_button .setText (self ._tr ("multipart_off_button_text","Multi-part: OFF"))
self .multipart_toggle_button .setToolTip (self ._tr ("multipart_off_button_tooltip","Tooltip for multipart OFF"))
+ def _update_error_button_count(self):
+ """Updates the Error button text to show the count of failed files."""
+ if not hasattr(self, 'error_btn'):
+ return
+
+ count = len(self.permanently_failed_files_for_dialog)
+ base_text = self._tr("error_button_text", "Error")
+
+ if count > 0:
+ self.error_btn.setText(f"({count}) {base_text}")
+ else:
+ self.error_btn.setText(base_text)
+
def _toggle_multipart_mode (self ):
if not self .allow_multipart_download_setting :
msg_box =QMessageBox (self )