This commit is contained in:
Yuvi9587
2025-07-11 01:24:12 -07:00
parent fa198c41c1
commit bcf26bea20
5 changed files with 482 additions and 293 deletions

View File

@@ -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. These are the primary controls you'll interact with to initiate and manage downloads.
### 1.1. Core Inputs ### 1.1. Core Inputs
**🔗 Creator/Post URL Input Field** **🔗 Creator/Post URL Input Field**  
- **Purpose**: Paste the URL of the content you want to download. - **Purpose**: Paste the URL of the content you want to download.  
- **Supported Sites**: Kemono.su, Coomer.party, Simpcity.su. - **Supported Sites**: Kemono.su, Coomer.party, Simpcity.su.  
- **Supported URL Types**: - **Supported URL Types**:  
- Creator pages (e.g., `https://kemono.su/patreon/user/12345`).   - Creator pages (e.g., `https://kemono.su/patreon/user/12345`).  
- Individual posts (e.g., `https://kemono.su/patreon/user/12345/post/98765`).   - 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. - **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** **🎨 Creator Selection Button**  
- **Icon**: 🎨 (Artist Palette) - **Icon**: 🎨 (Artist Palette)  
- **Purpose**: Opens the "Creator Selection" dialog to browse and queue downloads from known creators. - **Purpose**: Opens the "Creator Selection" dialog to browse and queue downloads from known creators.  
- **Dialog Features**: - **Dialog Features**:  
- Loads creators from `creators.json`.   - Loads creators from `creators.json`.  
- **Search Bar**: Filter creators by name.   - **Search Bar**: Filter creators by name.  
- **Creator List**: Displays creators with their service (e.g., Patreon, Fanbox).   - **Creator List**: Displays creators with their service (e.g., Patreon, Fanbox).  
- **Selection**: Checkboxes to select one or more creators.   - **Selection**: Checkboxes to select one or more creators.  
- **Download Scope**: Organize downloads by Characters or Creators.   - **Download Scope**: Organize downloads by Characters or Creators.  
- **Add to Queue**: Adds selected creators or their posts to the download queue.   - **Add to Queue**: Adds selected creators or their posts to the download queue.
**Page Range (Start to End) Input Fields** **Page Range (Start to End) Input Fields**  
- **Purpose**: Specify a range of pages to fetch for creator URLs. - **Purpose**: Specify a range of pages to fetch for creator URLs.  
- **Usage**: Enter the starting and ending page numbers. - **Usage**: Enter the starting and ending page numbers.  
- **Behavior**: - **Behavior**:  
- If blank, all pages are processed.   - If blank, all pages are processed.  
- Disabled for single post URLs.   - Disabled for single post URLs.
**📁 Download Location Input Field & Browse Button** **📁 Download Location Input Field & Browse Button**  
- **Purpose**: Specify the main directory for downloaded files. - **Purpose**: Specify the main directory for downloaded files.  
- **Usage**: Type the path or click "Browse..." to select a folder. - **Usage**: Type the path or click "Browse..." to select a folder.  
- **Requirement**: Mandatory for all download operations. - **Requirement**: Mandatory for all download operations.
### 1.2. Action Buttons ### 1.2. Action Buttons
**⬇️ Start Download / 🔗 Extract Links Button** **⬇️ Start Download / 🔗 Extract Links Button**  
- **Purpose**: Initiates downloading or link extraction. - **Purpose**: Initiates downloading or link extraction.  
- **Behavior**: - **Behavior**:  
- Shows "🔗 Extract Links" if "Only Links" is selected.   - Shows "🔗 Extract Links" if "Only Links" is selected.  
- Otherwise, shows "⬇️ Start Download".   - Otherwise, shows "⬇️ Start Download".  
- Supports single-threaded or multi-threaded downloads based on settings.   - Supports single-threaded or multi-threaded downloads based on settings.
**🔄 Restore Download Button** **🔄 Restore Download Button**  
- **Visibility**: Appears if an incomplete session is detected on startup. - **Visibility**: Appears if an incomplete session is detected on startup.  
- **Purpose**: Resumes a previously interrupted download session. - **Purpose**: Resumes a previously interrupted download session.
**⏸️ Pause / ▶️ Resume Download Button** **⏸️ Pause / ▶️ Resume Download Button**  
- **Purpose**: Pause or resume the ongoing download. - **Purpose**: Pause or resume the ongoing download.  
- **Behavior**: Toggles between "Pause" and "Resume". Some UI settings can be changed while paused. - **Behavior**: Toggles between "Pause" and "Resume". Some UI settings can be changed while paused.
**❌ Cancel & Reset UI Button** **❌ Cancel & Reset UI Button**  
- **Purpose**: Stops the current operation and performs a "soft" reset. - **Purpose**: Stops the current operation and performs a "soft" reset.  
- **Behavior**: Halts background threads, preserves URL and Download Location inputs, resets other settings. - **Behavior**: Halts background threads, preserves URL and Download Location inputs, resets other settings.
**🔄 Reset Button (in the log area)** **🔄 Reset Button (in the log area)**  
- **Purpose**: Performs a "hard" reset when no operation is active. - **Purpose**: Performs a "hard" reset when no operation is active.  
- **Behavior**: Clears all inputs, resets options to default, and clears logs. - **Behavior**: Clears all inputs, resets options to default, and clears logs.
## 2. Filtering & Content Selection ## 2. Filtering & Content Selection
These options allow precise control over downloaded content. These options allow precise control over downloaded content.
### 2.1. Content Filtering ### 2.1. Content Filtering
**🎯 Filter by Character(s) Input Field** **🎯 Filter by Character(s) Input Field**  
- **Purpose**: Download content related to specific characters or series. - **Purpose**: Download content related to specific characters or series.  
- **Usage**: Enter comma-separated character names. - **Usage**: Enter comma-separated character names.  
- **Advanced Syntax**: - **Advanced Syntax**:  
- `Nami`: Simple filter.   - `Nami`: Simple filter.  
- `(Vivi, Ulti)`: Grouped filter. Matches posts with "Vivi" OR "Ulti". Creates a shared folder like `Vivi Ulti` if subfolders are enabled.   - `(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.   - `(Boa, Hancock)~`: Aliased filter. Treats "Boa" and "Hancock" as the same entity.
**Filter: [Type] Button (Character Filter Scope)** **Filter: [Type] Button (Character Filter Scope)**  
- **Purpose**: Defines where the character filter is applied. Cycles on click. - **Purpose**: Defines where the character filter is applied. Cycles on click.  
- **Options**: - **Options**:  
- **Filter: Title** (Default): Matches post titles.   - **Filter: Title** (Default): Matches post titles.  
- **Filter: Files**: Matches filenames.   - **Filter: Files**: Matches filenames.  
- **Filter: Both**: Checks title first, then filenames.   - **Filter: Both**: Checks title first, then filenames.  
- **Filter: Comments (Beta)**: Checks filenames, then post comments.   - **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`). - **Purpose**: Exclude posts/files with specified keywords (e.g., `WIP`, `sketch`).
**Scope: [Type] Button (Skip Words Scope)** **Scope: [Type] Button (Skip Words Scope)**  
- **Purpose**: Defines where skip words are applied. Cycles on click. - **Purpose**: Defines where skip words are applied. Cycles on click.  
- **Options**: - **Options**:  
- **Scope: Posts** (Default): Skips posts if the title contains a skip word.   - **Scope: Posts** (Default): Skips posts if the title contains a skip word.  
- **Scope: Files**: Skips files if the filename contains a skip word.   - **Scope: Files**: Skips files if the filename contains a skip word.  
- **Scope: Both**: Applies both rules.   - **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]`). - **Purpose**: Remove unwanted text from filenames (e.g., `patreon`, `[HD]`).
### 2.2. File Type Filtering ### 2.2. File Type Filtering
**Filter Files (Radio Buttons)** **Filter Files (Radio Buttons)**  
- **Purpose**: Select file types to download. - **Purpose**: Select file types to download.  
- **Options**: - **Options**:  
- **All**: All file types.   - **All**: All file types.  
- **Images/GIFs**: Common image formats.   - **Images/GIFs**: Common image formats.  
- **Videos**: Common video formats.   - **Videos**: Common video formats.  
- **🎧 Only Audio**: Common audio formats.   - **🎧 Only Audio**: Common audio formats.  
- **📦 Only Archives**: Only `.zip` and `.rar` files.   - **📦 Only Archives**: Only `.zip` and `.rar` files.  
- **🔗 Only Links**: Extracts external links without downloading files.   - **🔗 Only Links**: Extracts external links without downloading files.
**Skip .zip / Skip .rar Checkboxes** **Skip .zip / Skip .rar Checkboxes**  
- **Purpose**: Skip downloading `.zip` or `.rar` files. - **Purpose**: Skip downloading `.zip` or `.rar` files.  
- **Behavior**: Disabled when "📦 Only Archives" is active. - **Behavior**: Disabled when "📦 Only Archives" is active.
## 3. Download Customization ## 3. Download Customization
Options to refine the download process and output. Options to refine the download process and output.
- **Download Thumbnails Only**: Downloads small preview images instead of full-resolution files. - **Download Thumbnails Only**: Downloads small preview images instead of full-resolution files.  
- **Scan Content for Images**: Scans post HTML for `<img>` tags, crucial for images in descriptions. - **Scan Content for Images**: Scans post HTML for `<img>` tags, crucial for images in descriptions.  
- **Compress to WebP**: Converts images to WebP format (requires Pillow library). - **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). - **🗄️ 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 ## 4. 📖 Manga/Comic Mode
A mode for downloading creator feeds in chronological order, ideal for sequential content. 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). - **Activation**: Active when downloading a creator's entire feed (not a single post).  
- **Core Behavior**: Fetches all posts, processing from oldest to newest. - **Core Behavior**: Fetches all posts, processing from oldest to newest.  
- **Filename Style Toggle Button (in the log area)**: - **Filename Style Toggle Button (in the log area)**:  
- **Purpose**: Controls file naming in Manga Mode. Cycles on click.   - **Purpose**: Controls file naming in Manga Mode. Cycles on click.  
- **Options**:   - **Options**:  
- **Name: Post Title**: First file named after post title; others keep original names.     - **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: 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: 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: 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: Post ID**: Files named after post ID to avoid clashes.  
- **Name: Date + Title**: Combines post date and title for filenames.     - **Name: Date + Title**: Combines post date and title for filenames.
## 5. Folder Organization & Known.txt ## 5. Folder Organization & Known.txt
Controls for structuring downloaded content. Controls for structuring downloaded content.
- **Separate Folders by Name/Title Checkbox**: Enables automatic subfolder creation. - **Separate Folders by Name/Title Checkbox**: Enables automatic subfolder creation.  
- **Subfolder per Post Checkbox**: Creates subfolders for each post, named after the post title. - **Subfolder per Post Checkbox**: Creates subfolders for each post, named after the post title.  
- **Known.txt Management UI (Bottom Left)**: - **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.
- **Purpose**: Manages a local `Known.txt` file for series, characters, or terms used in folder creation. - **Known.txt Management UI (Bottom Left)**:  
- **List Display**: Shows primary names from `Known.txt`.   - **Purpose**: Manages a local `Known.txt` file for series, characters, or terms used in folder creation.  
- ** Add Button**: Adds names or groups (e.g., `(Character A, Alias B)~`).   - **List Display**: Shows primary names from `Known.txt`.  
- **⤵️ Add to Filter Button**: Select names from `Known.txt` for the character filter.   - ** Add Button**: Adds names or groups (e.g., `(Character A, Alias B)~`).  
- **🗑 Delete Selected Button**: Removes selected names from `Known.txt`.   - ** Add to Filter Button**: Select names from `Known.txt` for the character filter.  
- **Open Known.txt Button**: Opens the file in the default text editor.   - **🗑️ Delete Selected Button**: Removes selected names from `Known.txt`.  
- **❓ Help Button**: Opens this feature guide.   - **Open Known.txt Button**: Opens the file in the default text editor.  
- **📜 History Button**: Views recent download history.   - ** Help Button**: Opens this feature guide.  
  - **📜 History Button**: Views recent download history.
## 6. ⭐ Favorite Mode (Kemono.su Only) ## 6. ⭐ Favorite Mode (Kemono.su Only)
Download from favorited artists/posts on Kemono.su. Download from favorited artists/posts on Kemono.su.
- **Enable Checkbox ("⭐ Favorite Mode")**: - **Enable Checkbox ("⭐ Favorite Mode")**:  
- Switches to Favorite Mode.   - Switches to Favorite Mode.  
- Disables the main URL input.   - Disables the main URL input.  
- Changes action buttons to "Favorite Artists" and "Favorite Posts".   - Changes action buttons to "Favorite Artists" and "Favorite Posts".  
- Requires cookies.   - Requires cookies.  
- **🖼️ Favorite Artists Button**: Select and download from favorited artists. - **🖼️ Favorite Artists Button**: Select and download from favorited artists.  
- **📄 Favorite Posts Button**: Select and download specific favorited posts. - **📄 Favorite Posts Button**: Select and download specific favorited posts.  
- **Favorite Download Scope Button**: - **Favorite Download Scope Button**:  
- **Scope: Selected Location**: Downloads favorites to the main directory.   - **Scope: Selected Location**: Downloads favorites to the main directory.  
- **Scope: Artist Folders**: Creates subfolders per artist.   - **Scope: Artist Folders**: Creates subfolders per artist.
## 7. Advanced Settings & Performance ## 7. Advanced Settings & Performance
- **🍪 Cookie Management**: - **🍪 Cookie Management**:  
- **Use Cookie Checkbox**: Enables cookies for restricted content.   - **Use Cookie Checkbox**: Enables cookies for restricted content.  
- **Cookie Text Field**: Paste cookie string.   - **Cookie Text Field**: Paste cookie string.  
- **Browse... Button**: Select a `cookies.txt` file (Netscape format).   - **Browse... Button**: Select a `cookies.txt` file (Netscape format).  
- **Use Multithreading Checkbox & Threads Input**: - **Use Multithreading Checkbox & Threads Input**:  
- **Purpose**: Configures simultaneous operations.   - **Purpose**: Configures simultaneous operations.  
- **Behavior**: Sets concurrent post processing (creator feeds) or file downloads (single posts).   - **Behavior**: Sets concurrent post processing (creator feeds) or file downloads (single posts).  
- **Multi-part Download Toggle Button**: - **Multi-part Download Toggle Button**:  
- **Purpose**: Enables/disables multi-segment downloading for large files.   - **Purpose**: Enables/disables multi-segment downloading for large files.  
- **Note**: Best for large files; less efficient for small files.   - **Note**: Best for large files; less efficient for small files.
## 8. Logging, Monitoring & Error Handling ## 8. Logging, Monitoring & Error Handling
- **📜 Progress Log Area**: Displays messages, progress, and errors. - **📜 Progress Log Area**: Displays messages, progress, and errors.  
- **👁️ / 🙈 Log View Toggle Button**: Switches between Progress Log and Missed Character Log (skipped posts). - **👁️ / 🙈 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. - **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. - **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. - **Download Extracted Links Button**: Downloads files from supported external links in "Only Links" mode.  
- **🆘 Error Button & Dialog**: - **🆘 Error Button & Dialog**:  
- **Purpose**: Active if files fail to download.   - **Purpose**: Active if files fail to download. The button will display a live count of failed files (e.g., **(3) Error**).  
- **Dialog Features**:   - **Dialog Features**:  
- Lists failed files.     - Lists failed files.  
- Retry failed downloads.     - Retry failed downloads.  
- Export failed URLs to a text file.     - Export failed URLs to a text file.
## 9. Application Settings (⚙️) ## 9. Application Settings (⚙️)
- **Appearance**: Switch between Light and Dark themes. - **Appearance**: Switch between Light and Dark themes.  
- **Language**: Change UI language (restart required). - **Language**: Change UI language (restart required).

182
readme.md
View File

@@ -1,45 +1,42 @@
<h1 align="center">Kemono Downloader v5.6.0</h1> <h1 align="center">Kemono Downloader v6.0.0</h1>
<table align="center"> <table align="center">
<tr>   <tr>
<td align="center">     <td align="center">
<img src="Read/Read.png" alt="Default Mode" width="400"/><br>       <img src="Read/Read.png" alt="Default Mode" width="400"/><br>
<strong>Default</strong>       <strong>Default</strong>
</td>     </td>
<td align="center">     <td align="center">
<img src="Read/Read1.png" alt="Favorite Mode" width="400"/><br>       <img src="Read/Read1.png" alt="Favorite Mode" width="400"/><br>
<strong>Favorite mode</strong>       <strong>Favorite mode</strong>
</td>     </td>
</tr>   </tr>
<tr>   <tr>
<td align="center">     <td align="center">
<img src="Read/Read2.png" alt="Single Post" width="400"/><br>       <img src="Read/Read2.png" alt="Single Post" width="400"/><br>
<strong>Single Post</strong>       <strong>Single Post</strong>
</td>     </td>
<td align="center">     <td align="center">
<img src="Read/Read3.png" alt="Manga/Comic Mode" width="400"/><br>       <img src="Read/Read3.png" alt="Manga/Comic Mode" width="400"/><br>
<strong>Manga/Comic Mode</strong>       <strong>Manga/Comic Mode</strong>
</td>     </td>
</tr>   </tr>
</table> </table>
--- ---
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). 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. 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.*
<p align="center"> <p align="center">
<a href="features.md">   <a href="features.md">
<img alt="Features" src="https://img.shields.io/badge/📚%20Full%20Feature%20List-FFD700?style=for-the-badge&logoColor=black&color=FFD700">     <img alt="Features" src="https://img.shields.io/badge/📚%20Full%20Feature%20List-FFD700?style=for-the-badge&logoColor=black&color=FFD700">
</a>   </a>
<a href="LICENSE">   <a href="LICENSE">
<img alt="License" src="https://img.shields.io/badge/📝%20License-90EE90?style=for-the-badge&logoColor=black&color=90EE90">     <img alt="License" src="https://img.shields.io/badge/📝%20License-90EE90?style=for-the-badge&logoColor=black&color=90EE90">
</a>   </a>
<a href="note.md">   <a href="note.md">
<img alt="Note" src="https://img.shields.io/badge/⚠️%20Important%20Note-FFCCCB?style=for-the-badge&logoColor=black&color=FFCCCB">     <img alt="Note" src="https://img.shields.io/badge/⚠️%20Important%20Note-FFCCCB?style=for-the-badge&logoColor=black&color=FFCCCB">
</a>   </a>
</p> </p>
--- ---
@@ -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: 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. -   **User-Friendly Interface:** A modern PyQt5 GUI for easy navigation and operation.
- **Flexible Downloading:** -   **Flexible Downloading:**
- Download content from Kemono.su (and mirrors) and Coomer.party (and mirrors).     -   Download content from Kemono.su (and mirrors) and Coomer.party (and mirrors).
- Supports creator pages (with page range selection) and individual post URLs.     -   Supports creator pages (with page range selection) and individual post URLs.
- Standard download controls: Start, Pause, Resume, and Cancel.     -   Standard download controls: Start, Pause, Resume, and Cancel.
- **Powerful Filtering:** -   **Powerful Filtering:**
- **Character Filtering:** Filter content by character names. Supports simple comma-separated names and grouped names for shared folders.     -   **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.     -   **Keyword Skipping:** Skip posts or files based on specified keywords.
- **Filename Cleaning:** Remove unwanted words or phrases from downloaded filenames.     -   **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.     -   **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:** -   **Customizable Downloads:**
- **Thumbnails Only:** Option to download only small preview images.     -   **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.     -   **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).     -   **WebP Conversion:** Convert images to WebP format for smaller file sizes (requires Pillow library).
- **Organized Output:** -   **Organized Output:**
- **Automatic Subfolders:** Create subfolders based on character names (from filters or `Known.txt`) or post titles.     -   **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.     -   **Per-Post Subfolders:** Option to create an additional subfolder for each individual post.
- **Manga/Comic Mode:** -   **Manga/Comic Mode:**
- Downloads posts from a creator's feed in chronological order (oldest to newest).     -   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).     -   Offers various filename styling options for sequential reading (e.g., post title, original name, global numbering).
- **⭐ Favorite Mode:** -   **⭐ Favorite Mode:**
- Directly download from your favorited artists and posts on Kemono.su.     -   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.     -   Requires a valid cookie and adapts the UI for easy selection from your favorites.
- Supports downloading into a single location or artist-specific subfolders.     -   Supports downloading into a single location or artist-specific subfolders. 
- **Performance & Advanced Options:** -   **Performance & Advanced Options:**
- **Cookie Support:** Use cookies (paste string or load from `cookies.txt`) to access restricted content.     -   **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.     -   **Multithreading:** Configure the number of simultaneous downloads/post processing threads for improved speed.
- **Logging:** -   **Logging:**
- A detailed progress log displays download activity, errors, and summaries.     -   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). -   **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. -   **Theme Customization:** Selectable Light and Dark themes for user comfort.
--- ---
## ✨ What's New in v5.3.0 ## ✨ What's New in v6.0.0
- **Multi-Creator Post Fetching & Queuing:**
- The **Creator Selection popup** (🎨 icon) has been significantly enhanced. This release focuses on providing more granular control over file organization and improving at-a-glance status monitoring.
- 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. ### New Features
- You can then browse these fetched posts (with search functionality) and select individual posts. - **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.
- 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. - **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.
- The traditional "**Add Selected to URL**" button is still available if you prefer to populate the main URL field with creator names. - **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.
- **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"). ### Bug Fixes & Quality of Life
- The queue is now more robustly managed, especially when interacting with the main URL input field after items have been queued from the popup. - 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 ## Next Update Plans
- **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.
## ✨ What's New in v5.2.0 ### 🔖 Post Tag Filtering (Planned for v6.1.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. A new **"Filter by Post Tags"** feature will allow users to:
- **Theme Selection:** Choose between Light and Dark application themes via the Settings dialog for a personalized viewing experience. - Filter and download content based on specific post tags
- **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. - Combine tag filtering with existing character filters and file types.
- **Internal Localization:** Introduced `languages.py` for managing UI translations, streamlining the addition of new languages by contributors. - 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 ## Installation
### Requirements ### Requirements
- Python 3.6 or higher -   Python 3.6 or higher
- pip (Python package installer) -   pip (Python package installer)
### Install Dependencies ### Install Dependencies
Open your terminal or command prompt and run: Open your terminal or command prompt and run:

View File

@@ -78,6 +78,7 @@ class PostProcessorWorker:
creator_download_folder_ignore_words =None , creator_download_folder_ignore_words =None ,
manga_global_file_counter_ref =None , manga_global_file_counter_ref =None ,
use_date_prefix_for_subfolder=False, use_date_prefix_for_subfolder=False,
keep_in_post_duplicates=False,
session_file_path=None, session_file_path=None,
session_lock=None, session_lock=None,
): ):
@@ -130,6 +131,7 @@ class PostProcessorWorker:
self .scan_content_for_images =scan_content_for_images self .scan_content_for_images =scan_content_for_images
self .creator_download_folder_ignore_words =creator_download_folder_ignore_words self .creator_download_folder_ignore_words =creator_download_folder_ignore_words
self.use_date_prefix_for_subfolder = use_date_prefix_for_subfolder 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_file_path = session_file_path
self.session_lock = session_lock self.session_lock = session_lock
if self .compress_images and Image is None : 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 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 )) 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 ): # Rescue download if an IncompleteRead error occurred but the file is complete
self .logger (f" ⚠️ Download process interrupted for {api_original_filename }.") if (not download_successful_flag and
if downloaded_part_file_path and os .path .exists (downloaded_part_file_path ): isinstance(last_exception_for_retry_later, http.client.IncompleteRead) and
try :os .remove (downloaded_part_file_path ) total_size_bytes > 0 and downloaded_part_file_path and os.path.exists(downloaded_part_file_path)):
except OSError :pass try:
return 0 ,1 ,filename_to_save_in_main_path ,was_original_name_kept_flag ,FILE_DOWNLOAD_STATUS_SKIPPED ,None 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 : 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 failed for '{api_original_filename }' after {max_retries +1 } attempts.") 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 with self.downloaded_file_hashes_lock:
if isinstance (last_exception_for_retry_later ,http .client .IncompleteRead ): if calculated_file_hash in self.downloaded_file_hashes:
is_actually_incomplete_read =True self.logger(f" -> Skip Saving Duplicate (Hash Match): '{api_original_filename}' (Hash: {calculated_file_hash[:8]}...).")
elif hasattr (last_exception_for_retry_later ,'__cause__')and isinstance (last_exception_for_retry_later .__cause__ ,http .client .IncompleteRead ): with self.downloaded_files_lock: self.downloaded_files.add(filename_to_save_in_main_path)
is_actually_incomplete_read =True 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 : effective_save_folder = target_folder_path
str_exc =str (last_exception_for_retry_later ).lower () 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 )))): try:
is_actually_incomplete_read =True 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 : data_to_write_io = None
self .logger (f" Marking '{api_original_filename }' for potential retry later due to IncompleteRead.") filename_after_compression = filename_after_styling_and_word_removal
retry_later_details ={ is_img_for_compress_check = is_image(api_original_filename)
'file_info':file_info ,
'target_folder_path':target_folder_path , if is_img_for_compress_check and self.compress_images and Image and downloaded_size_bytes > (1.5 * 1024 * 1024):
'headers':headers , # ... (This block for image compression remains the same)
'original_post_id_for_log':original_post_id_for_log , self .logger (f" Compressing '{api_original_filename }' ({downloaded_size_bytes /(1024 *1024 ):.2f} MB)...")
'post_title':post_title , 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
'file_index_in_post':file_index_in_post , img_content_for_pillow =None
'num_files_in_this_post':num_files_in_this_post , try :
'forced_filename_override':filename_to_save_in_main_path , with open (downloaded_part_file_path ,'rb')as f_img_in :
'manga_mode_active_for_file':self .manga_mode_active , img_content_for_pillow =BytesIO (f_img_in .read ())
'manga_filename_style_for_file':self .manga_filename_style , 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 <downloaded_size_bytes *0.9 :
self .logger (f" Compression success: {compressed_size /(1024 *1024 ):.2f} MB.")
data_to_write_io =compressed_output_io
data_to_write_io .seek (0 )
base_name_orig ,_ =os .path .splitext (filename_after_compression )
filename_after_compression =base_name_orig +'.webp'
self .logger (f" Updated filename (compressed): {filename_after_compression }")
else :
self .logger (f" Compression skipped: WebP not significantly smaller.")
if compressed_output_io :compressed_output_io .close ()
except Exception as comp_e :
self .logger (f"❌ Compression failed for '{api_original_filename }': {comp_e }. Saving original.")
finally :
if img_content_for_pillow :img_content_for_pillow .close ()
final_filename_on_disk = filename_after_compression
temp_base, temp_ext = os.path.splitext(final_filename_on_disk)
suffix_counter = 1
while os.path.exists(os.path.join(effective_save_folder, final_filename_on_disk)):
final_filename_on_disk = f"{temp_base}_{suffix_counter}{temp_ext}"
suffix_counter += 1
if final_filename_on_disk != filename_after_compression:
self.logger(f" Applied numeric suffix in '{os.path.basename(effective_save_folder)}': '{final_filename_on_disk}' (was '{filename_after_compression}')")
if self._check_pause(f"File saving for '{final_filename_on_disk}'"):
return 0, 1, final_filename_on_disk, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
final_save_path = os.path.join(effective_save_folder, final_filename_on_disk)
try:
if data_to_write_io:
with open(final_save_path, 'wb') as f_out:
time.sleep(0.05)
f_out.write(data_to_write_io.getvalue())
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 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 self._emit_signal('file_successfully_downloaded', downloaded_file_details)
else : time.sleep(0.05)
self .logger (f" Marking '{api_original_filename }' as permanently failed for this session.")
permanent_failure_details ={ return 1, 0, final_filename_saved_for_return, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SUCCESS, None
'file_info':file_info , except Exception as save_err:
'target_folder_path':target_folder_path , self.logger(f"->>Save Fail for '{final_filename_on_disk}': {save_err}")
'headers':headers , if os.path.exists(final_save_path):
'original_post_id_for_log':original_post_id_for_log , try: os.remove(final_save_path)
'post_title':post_title , except OSError: self.logger(f" -> Failed to remove partially saved file: {final_save_path}")
'file_index_in_post':file_index_in_post , return 0, 1, final_filename_saved_for_return, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
'num_files_in_this_post':num_files_in_this_post , finally:
'forced_filename_override':filename_to_save_in_main_path , if data_to_write_io and hasattr(data_to_write_io, 'close'):
} data_to_write_io.close()
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 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 : with self .downloaded_file_hashes_lock :
if calculated_file_hash in self .downloaded_file_hashes : 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 ]}...).") 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 return 0 ,0 ,[],[],[],None
files_to_download_info_list =[] files_to_download_info_list =[]
processed_original_filenames_in_this_post =set () 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 self.keep_in_post_duplicates:
if current_api_original_filename in processed_original_filenames_in_this_post : # If we keep duplicates, just add every file to the list to be processed.
self .logger (f" -> Skip Duplicate Original Name (within post {post_id }): '{current_api_original_filename }' already processed/listed for this post.") # The downstream hash check and rename-on-collision logic will handle them.
total_skipped_this_post +=1 files_to_download_info_list.extend(all_files_from_post_api)
else : self.logger(f" 'Keep Duplicates' is on. All {len(all_files_from_post_api)} files from post will be processed.")
files_to_download_info_list .append (file_info ) else:
if current_api_original_filename : # This is the original logic that skips duplicates by name within a post.
processed_original_filenames_in_this_post .add (current_api_original_filename ) for file_info in all_files_from_post_api:
if not files_to_download_info_list : 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.") self .logger (f" All files for post {post_id } were duplicate original names or skipped earlier.")
return 0 ,total_skipped_this_post ,[],[],[],None 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: with open(self.session_file_path, 'r', encoding='utf-8') as f:
session_data = json.load(f) session_data = json.load(f)
# Modify in memory if 'download_state' not in session_data:
if not isinstance(session_data.get('download_state', {}).get('processed_post_ids'), list): 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'] = []
session_data['download_state']['processed_post_ids'].append(self.post.get('id')) 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 # Write to temp file and then atomically replace
temp_file_path = self.session_file_path + ".tmp" temp_file_path = self.session_file_path + ".tmp"
with open(temp_file_path, 'w', encoding='utf-8') as f_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 , scan_content_for_images =False ,
creator_download_folder_ignore_words =None , creator_download_folder_ignore_words =None ,
use_date_prefix_for_subfolder=False, use_date_prefix_for_subfolder=False,
keep_in_post_duplicates=False,
cookie_text ="", cookie_text ="",
session_file_path=None, session_file_path=None,
session_lock=None, session_lock=None,
@@ -1513,6 +1655,7 @@ class DownloadThread (QThread ):
self .scan_content_for_images =scan_content_for_images self .scan_content_for_images =scan_content_for_images
self .creator_download_folder_ignore_words =creator_download_folder_ignore_words self .creator_download_folder_ignore_words =creator_download_folder_ignore_words
self.use_date_prefix_for_subfolder = use_date_prefix_for_subfolder 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 .manga_global_file_counter_ref =manga_global_file_counter_ref
self.session_file_path = session_file_path self.session_file_path = session_file_path
self.session_lock = session_lock self.session_lock = session_lock
@@ -1646,6 +1789,7 @@ class DownloadThread (QThread ):
use_cookie =self .use_cookie , use_cookie =self .use_cookie ,
manga_date_file_counter_ref =self .manga_date_file_counter_ref , manga_date_file_counter_ref =self .manga_date_file_counter_ref ,
use_date_prefix_for_subfolder=self.use_date_prefix_for_subfolder, 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 , creator_download_folder_ignore_words =self .creator_download_folder_ignore_words ,
session_file_path=self.session_file_path, session_file_path=self.session_file_path,
session_lock=self.session_lock, session_lock=self.session_lock,

View File

@@ -4,7 +4,7 @@ import sys
# --- PyQt5 Imports --- # --- PyQt5 Imports ---
from PyQt5.QtCore import QUrl, QSize, Qt from PyQt5.QtCore import QUrl, QSize, Qt
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon, QDesktopServices
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
QStackedWidget, QScrollArea, QFrame, QWidget QStackedWidget, QScrollArea, QFrame, QWidget

View File

@@ -241,7 +241,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 v5.5.0") self.setWindowTitle("Kemono Downloader v6.0.0")
self.init_ui() self.init_ui()
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.")
@@ -274,6 +274,8 @@ class DownloaderApp (QWidget ):
'use_subfolder_per_post_checkbox': 'use_post_subfolders', 'use_subfolder_per_post_checkbox': 'use_post_subfolders',
'use_multithreading_checkbox': 'use_multithreading', 'use_multithreading_checkbox': 'use_multithreading',
'external_links_checkbox': 'show_external_links', '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', 'manga_mode_checkbox': 'manga_mode_active',
'scan_content_images_checkbox': 'scan_content_for_images', 'scan_content_images_checkbox': 'scan_content_for_images',
'use_cookie_checkbox': 'use_cookie', '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: if "ui_settings" not in session_data or "download_state" not in session_data:
raise ValueError("Invalid session file structure.") 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.interrupted_session_data = session_data
self.log_signal.emit(" Incomplete download session found. UI updated for restore.") self.log_signal.emit(" Incomplete download session found. UI updated for restore.")
self._prepare_ui_for_restore() self._prepare_ui_for_restore()
@@ -422,11 +430,13 @@ class DownloaderApp (QWidget ):
self.pause_btn.setEnabled(True) self.pause_btn.setEnabled(True)
self.pause_btn.clicked.connect(self.restore_download) 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.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")) # --- START: CORRECTED CANCEL BUTTON LOGIC ---
self.cancel_btn.setEnabled(False) # Nothing to cancel yet self.cancel_btn.setText(self._tr("discard_session_button_text", "🗑️ Discard Session"))
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).")) 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: elif is_download_active:
# State: Downloading / Paused # State: Downloading / Paused
self.download_btn.setText(self._tr("start_download_button_text", "⬇️ Start Download")) 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).") self .compress_images_checkbox .setToolTip ("Compress images > 1.5MB to WebP format (requires Pillow).")
row1_layout .addWidget (self .compress_images_checkbox ) 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 ) row1_layout .addStretch (1 )
checkboxes_group_layout .addLayout (row1_layout ) checkboxes_group_layout .addLayout (row1_layout )
@@ -3578,6 +3593,7 @@ class DownloaderApp (QWidget ):
self .retryable_failed_files_info .clear () self .retryable_failed_files_info .clear ()
self .permanently_failed_files_for_dialog .clear () self .permanently_failed_files_for_dialog .clear ()
self._update_error_button_count()
manga_date_file_counter_ref_for_thread =None manga_date_file_counter_ref_for_thread =None
if manga_mode and self .manga_filename_style ==STYLE_DATE_BASED and not extract_links_only : 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 : if not extract_links_only :
log_messages .append (f" Subfolders: {'Enabled'if use_subfolders else 'Disabled'}") 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 use_subfolders :
if custom_folder_name_cleaned :log_messages .append (f" Custom Folder (Post): '{custom_folder_name_cleaned }'") if custom_folder_name_cleaned :log_messages .append (f" Custom Folder (Post): '{custom_folder_name_cleaned }'")
if actual_filters_to_use_for_run : 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 .append (f" Folder Naming: Automatic (based on title/known names)")
log_messages .extend ([ keep_duplicates = self.keep_duplicates_checkbox.isChecked() if hasattr(self, 'keep_duplicates_checkbox') else False
f" File Type Filter: {user_selected_filter_text } (Backend processing as: {backend_filter_mode })", log_messages.extend([
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" File Type Filter: {user_selected_filter_text} (Backend processing as: {backend_filter_mode})",
f" Skip Words (posts/files): {', '.join (skip_words_list )if skip_words_list else 'None'}", f" Keep In-Post Duplicates: {'Enabled' if keep_duplicates else 'Disabled'}",
f" Skip Words Scope: {current_skip_words_scope .capitalize ()}", 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" Remove Words from Filename: {', '.join (remove_from_filename_words_list )if remove_from_filename_words_list else 'None'}", f" Skip Words Scope: {current_skip_words_scope .capitalize ()}",
f" Compress Images: {'Enabled'if compress_images else 'Disabled'}", f" Remove Words from Filename: {', '.join (remove_from_filename_words_list )if remove_from_filename_words_list else 'None'}",
f" Thumbnails Only: {'Enabled'if download_thumbnails else 'Disabled'}" 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'}") log_messages .append (f" Scan Post Content for Images: {'Enabled'if scan_content_for_images else 'Disabled'}")
else : else :
@@ -3731,6 +3751,7 @@ class DownloaderApp (QWidget ):
'session_lock': self.session_lock, 'session_lock': self.session_lock,
'creator_download_folder_ignore_words':creator_folder_ignore_words_for_run , '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, '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 args_template ['override_output_dir']=override_output_dir
@@ -3830,6 +3851,7 @@ class DownloaderApp (QWidget ):
dialog .exec_ () dialog .exec_ ()
def _handle_retry_from_error_dialog (self ,selected_files_to_retry ): 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 ._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 ): def _handle_retryable_file_failure (self ,list_of_retry_details ):
"""Appends details of files that failed but might be retryable later.""" """Appends details of files that failed but might be retryable later."""
@@ -3841,6 +3863,7 @@ class DownloaderApp (QWidget ):
if list_of_permanent_failure_details : if list_of_permanent_failure_details :
self .permanently_failed_files_for_dialog .extend (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 .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 ): 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.""" """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_filename_style',
'manga_date_prefix', 'manga_date_prefix',
'use_date_prefix_for_subfolder', 'use_date_prefix_for_subfolder',
'keep_in_post_duplicates',
'manga_global_file_counter_ref', 'manga_global_file_counter_ref',
'creator_download_folder_ignore_words', 'creator_download_folder_ignore_words',
'session_file_path', 'session_file_path',
@@ -4242,6 +4266,7 @@ class DownloaderApp (QWidget ):
self ._add_to_history_candidates (history_data_from_worker ) self ._add_to_history_candidates (history_data_from_worker )
if permanent_failures_from_post : if permanent_failures_from_post :
self .permanently_failed_files_for_dialog .extend (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 ) self ._add_to_history_candidates (history_data_from_worker )
with self .downloaded_files_lock : with self .downloaded_files_lock :
self .download_counter +=downloaded_files_from_future self .download_counter +=downloaded_files_from_future
@@ -4955,6 +4980,7 @@ class DownloaderApp (QWidget ):
# --- Reset UI and all state --- # --- Reset UI and all state ---
self.log_signal.emit("🔄 Resetting application state to defaults...") self.log_signal.emit("🔄 Resetting application state to defaults...")
self._reset_ui_to_defaults() self._reset_ui_to_defaults()
self._load_saved_download_location()
self.main_log_output.clear() self.main_log_output.clear()
self.external_log_output.clear() self.external_log_output.clear()
if self.missed_character_log_output: if self.missed_character_log_output:
@@ -4989,6 +5015,7 @@ class DownloaderApp (QWidget ):
self.only_links_log_display_mode = LOG_DISPLAY_LINKS self.only_links_log_display_mode = LOG_DISPLAY_LINKS
self.mega_download_log_preserved_once = False self.mega_download_log_preserved_once = False
self.permanently_failed_files_for_dialog.clear() self.permanently_failed_files_for_dialog.clear()
self._update_error_button_count()
self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION
self._update_favorite_scope_button_text() self._update_favorite_scope_button_text()
self.retryable_failed_files_info.clear() self.retryable_failed_files_info.clear()
@@ -5024,7 +5051,6 @@ class DownloaderApp (QWidget ):
"""Resets all UI elements and relevant state to their default values.""" """Resets all UI elements and relevant state to their default values."""
# Clear all text fields # Clear all text fields
self.link_input.clear() self.link_input.clear()
self.dir_input.clear()
self.custom_folder_input.clear() self.custom_folder_input.clear()
self.character_input.clear() self.character_input.clear()
self.skip_words_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 .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")) 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 ): def _toggle_multipart_mode (self ):
if not self .allow_multipart_download_setting : if not self .allow_multipart_download_setting :
msg_box =QMessageBox (self ) msg_box =QMessageBox (self )