9 Commits

Author SHA1 Message Date
Yuvi9587
f4f07f038d readme.md 2025-05-21 08:09:18 +05:30
Yuvi9587
7443615848 readme.md 2025-05-21 08:08:54 +05:30
Yuvi9587
3d8f62e7fd readme.md 2025-05-21 08:08:25 +05:30
Yuvi9587
525098b840 readme.md 2025-05-21 08:06:47 +05:30
Yuvi9587
39e4496b67 Readme.md 2025-05-21 08:06:04 +05:30
Yuvi9587
d5bf27f8cc Add files via upload 2025-05-21 08:04:10 +05:30
Yuvi9587
4e7c1783ea readme.md 2025-05-20 23:34:56 +05:30
Yuvi9587
ad67860eab readme.md 2025-05-20 23:34:35 +05:30
Yuvi9587
33841395ba readme.md 2025-05-20 23:33:45 +05:30
10 changed files with 1002 additions and 3669 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

0
Known.txt Normal file
View File

BIN
Read.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because it is too large Load Diff

3413
main.py

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ import hashlib
import http.client import http.client
import traceback import traceback
import threading import threading
import queue # Import the missing 'queue' module
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
CHUNK_DOWNLOAD_RETRY_DELAY = 2 # Slightly reduced for faster retries if needed CHUNK_DOWNLOAD_RETRY_DELAY = 2 # Slightly reduced for faster retries if needed
@@ -14,7 +13,7 @@ DOWNLOAD_CHUNK_SIZE_ITER = 1024 * 256 # 256KB for iter_content within a chunk d
def _download_individual_chunk(chunk_url, temp_file_path, start_byte, end_byte, headers, def _download_individual_chunk(chunk_url, temp_file_path, start_byte, end_byte, headers,
part_num, total_parts, progress_data, cancellation_event, skip_event, pause_event, global_emit_time_ref, cookies_for_chunk, # Added cookies_for_chunk part_num, total_parts, progress_data, cancellation_event, skip_event,
logger_func, emitter=None, api_original_filename=None): # Renamed logger, signals to emitter logger_func, emitter=None, api_original_filename=None): # Renamed logger, signals to emitter
"""Downloads a single chunk of a file and writes it to the temp file.""" """Downloads a single chunk of a file and writes it to the temp file."""
if cancellation_event and cancellation_event.is_set(): if cancellation_event and cancellation_event.is_set():
@@ -24,23 +23,22 @@ def _download_individual_chunk(chunk_url, temp_file_path, start_byte, end_byte,
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Skip event triggered before start.") logger_func(f" [Chunk {part_num + 1}/{total_parts}] Skip event triggered before start.")
return 0, False return 0, False
if pause_event and pause_event.is_set():
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Download paused before start...")
while pause_event.is_set():
if cancellation_event and cancellation_event.is_set():
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Download cancelled while paused.")
return 0, False
time.sleep(0.2) # Shorter sleep for responsive resume
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Download resumed.")
chunk_headers = headers.copy() chunk_headers = headers.copy()
# end_byte can be -1 for 0-byte files, meaning download from start_byte to end of file (which is start_byte itself)
if end_byte != -1 : # For 0-byte files, end_byte might be -1, Range header should not be set or be 0-0 if end_byte != -1 : # For 0-byte files, end_byte might be -1, Range header should not be set or be 0-0
chunk_headers['Range'] = f"bytes={start_byte}-{end_byte}" chunk_headers['Range'] = f"bytes={start_byte}-{end_byte}"
elif start_byte == 0 and end_byte == -1: # Specifically for 0-byte files elif start_byte == 0 and end_byte == -1: # Specifically for 0-byte files
# Some servers might not like Range: bytes=0--1.
# For a 0-byte file, we might not even need a range header, or Range: bytes=0-0
# Let's try without for 0-byte, or rely on server to handle 0-0 if Content-Length was 0.
# If Content-Length was 0, the main function might handle it directly.
# This chunking logic is primarily for files > 0 bytes.
# For now, if end_byte is -1, it implies a 0-byte file, so we expect 0 bytes.
pass pass
bytes_this_chunk = 0 bytes_this_chunk = 0
last_progress_emit_time_for_chunk = time.time()
last_speed_calc_time = time.time() last_speed_calc_time = time.time()
bytes_at_last_speed_calc = 0 bytes_at_last_speed_calc = 0
@@ -51,25 +49,23 @@ def _download_individual_chunk(chunk_url, temp_file_path, start_byte, end_byte,
if skip_event and skip_event.is_set(): if skip_event and skip_event.is_set():
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Skip event during retry loop.") logger_func(f" [Chunk {part_num + 1}/{total_parts}] Skip event during retry loop.")
return bytes_this_chunk, False return bytes_this_chunk, False
if pause_event and pause_event.is_set():
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Paused during retry loop...")
while pause_event.is_set():
if cancellation_event and cancellation_event.is_set():
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Cancelled while paused in retry loop.")
return bytes_this_chunk, False
time.sleep(0.2)
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Resumed from retry loop pause.")
try: try:
if attempt > 0: if attempt > 0:
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Retrying download (Attempt {attempt}/{MAX_CHUNK_DOWNLOAD_RETRIES})...") logger_func(f" [Chunk {part_num + 1}/{total_parts}] Retrying download (Attempt {attempt}/{MAX_CHUNK_DOWNLOAD_RETRIES})...")
time.sleep(CHUNK_DOWNLOAD_RETRY_DELAY * (2 ** (attempt - 1))) time.sleep(CHUNK_DOWNLOAD_RETRY_DELAY * (2 ** (attempt - 1)))
# Reset speed calculation on retry
last_speed_calc_time = time.time() last_speed_calc_time = time.time()
bytes_at_last_speed_calc = bytes_this_chunk # Current progress of this chunk bytes_at_last_speed_calc = bytes_this_chunk # Current progress of this chunk
# Enhanced log message for chunk start
log_msg = f" 🚀 [Chunk {part_num + 1}/{total_parts}] Starting download: bytes {start_byte}-{end_byte if end_byte != -1 else 'EOF'}" log_msg = f" 🚀 [Chunk {part_num + 1}/{total_parts}] Starting download: bytes {start_byte}-{end_byte if end_byte != -1 else 'EOF'}"
logger_func(log_msg) logger_func(log_msg)
response = requests.get(chunk_url, headers=chunk_headers, timeout=(10, 120), stream=True, cookies=cookies_for_chunk) print(f"DEBUG_MULTIPART: {log_msg}") # Direct console print for debugging
response = requests.get(chunk_url, headers=chunk_headers, timeout=(10, 120), stream=True)
response.raise_for_status() response.raise_for_status()
# For 0-byte files, if end_byte was -1, we expect 0 content.
if start_byte == 0 and end_byte == -1 and int(response.headers.get('Content-Length', 0)) == 0: if start_byte == 0 and end_byte == -1 and int(response.headers.get('Content-Length', 0)) == 0:
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Confirmed 0-byte file.") logger_func(f" [Chunk {part_num + 1}/{total_parts}] Confirmed 0-byte file.")
with progress_data['lock']: with progress_data['lock']:
@@ -86,19 +82,12 @@ def _download_individual_chunk(chunk_url, temp_file_path, start_byte, end_byte,
if skip_event and skip_event.is_set(): if skip_event and skip_event.is_set():
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Skip event during data iteration.") logger_func(f" [Chunk {part_num + 1}/{total_parts}] Skip event during data iteration.")
return bytes_this_chunk, False return bytes_this_chunk, False
if pause_event and pause_event.is_set():
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Paused during data iteration...")
while pause_event.is_set():
if cancellation_event and cancellation_event.is_set():
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Cancelled while paused in data iteration.")
return bytes_this_chunk, False
time.sleep(0.2)
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Resumed from data iteration pause.")
if data_segment: if data_segment:
f.write(data_segment) f.write(data_segment)
bytes_this_chunk += len(data_segment) bytes_this_chunk += len(data_segment)
with progress_data['lock']: with progress_data['lock']:
# Increment both the chunk's downloaded and the overall downloaded
progress_data['total_downloaded_so_far'] += len(data_segment) progress_data['total_downloaded_so_far'] += len(data_segment)
progress_data['chunks_status'][part_num]['downloaded'] = bytes_this_chunk progress_data['chunks_status'][part_num]['downloaded'] = bytes_this_chunk
progress_data['chunks_status'][part_num]['active'] = True progress_data['chunks_status'][part_num]['active'] = True
@@ -110,14 +99,19 @@ def _download_individual_chunk(chunk_url, temp_file_path, start_byte, end_byte,
current_speed_bps = (bytes_delta * 8) / time_delta_speed if time_delta_speed > 0 else 0 current_speed_bps = (bytes_delta * 8) / time_delta_speed if time_delta_speed > 0 else 0
progress_data['chunks_status'][part_num]['speed_bps'] = current_speed_bps progress_data['chunks_status'][part_num]['speed_bps'] = current_speed_bps
last_speed_calc_time = current_time last_speed_calc_time = current_time
bytes_at_last_speed_calc = bytes_this_chunk bytes_at_last_speed_calc = bytes_this_chunk
if emitter and (current_time - global_emit_time_ref[0] > 0.25): # Max ~4Hz for the whole file
global_emit_time_ref[0] = current_time # Update shared last emit time # Emit progress more frequently from within the chunk download
status_list_copy = [dict(s) for s in progress_data['chunks_status']] # Make a deep enough copy if current_time - last_progress_emit_time_for_chunk > 0.1: # Emit up to 10 times/sec per chunk
if isinstance(emitter, queue.Queue): if emitter:
emitter.put({'type': 'file_progress', 'payload': (api_original_filename, status_list_copy)}) # Ensure we read the latest total downloaded from progress_data
elif hasattr(emitter, 'file_progress_signal'): # PostProcessorSignals-like # Send a copy of the chunks_status list
emitter.file_progress_signal.emit(api_original_filename, status_list_copy) status_list_copy = [dict(s) for s in progress_data['chunks_status']] # Make a deep enough copy
if isinstance(emitter, queue.Queue):
emitter.put({'type': 'file_progress', 'payload': (api_original_filename, status_list_copy)})
elif hasattr(emitter, 'file_progress_signal'): # PostProcessorSignals-like
emitter.file_progress_signal.emit(api_original_filename, status_list_copy)
last_progress_emit_time_for_chunk = current_time
return bytes_this_chunk, True return bytes_this_chunk, True
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout, http.client.IncompleteRead) as e: except (requests.exceptions.ConnectionError, requests.exceptions.Timeout, http.client.IncompleteRead) as e:
@@ -131,6 +125,8 @@ def _download_individual_chunk(chunk_url, temp_file_path, start_byte, end_byte,
except Exception as e: except Exception as e:
logger_func(f" ❌ [Chunk {part_num + 1}/{total_parts}] Unexpected error: {e}\n{traceback.format_exc(limit=1)}") logger_func(f" ❌ [Chunk {part_num + 1}/{total_parts}] Unexpected error: {e}\n{traceback.format_exc(limit=1)}")
return bytes_this_chunk, False return bytes_this_chunk, False
# Ensure final status is marked as inactive if loop finishes due to retries
with progress_data['lock']: with progress_data['lock']:
progress_data['chunks_status'][part_num]['active'] = False progress_data['chunks_status'][part_num]['active'] = False
progress_data['chunks_status'][part_num]['speed_bps'] = 0 progress_data['chunks_status'][part_num]['speed_bps'] = 0
@@ -138,8 +134,7 @@ def _download_individual_chunk(chunk_url, temp_file_path, start_byte, end_byte,
def download_file_in_parts(file_url, save_path, total_size, num_parts, headers, api_original_filename, def download_file_in_parts(file_url, save_path, total_size, num_parts, headers, api_original_filename,
emitter_for_multipart, cookies_for_chunk_session, # Added cookies_for_chunk_session emitter_for_multipart, cancellation_event, skip_event, logger_func): # Renamed signals, logger
cancellation_event, skip_event, logger_func, pause_event):
""" """
Downloads a file in multiple parts concurrently. Downloads a file in multiple parts concurrently.
Returns: (download_successful_flag, downloaded_bytes, calculated_file_hash, temp_file_handle_or_None) Returns: (download_successful_flag, downloaded_bytes, calculated_file_hash, temp_file_handle_or_None)
@@ -186,8 +181,7 @@ def download_file_in_parts(file_url, save_path, total_size, num_parts, headers,
{'id': i, 'downloaded': 0, 'total': chunk_actual_sizes[i] if i < len(chunk_actual_sizes) else 0, 'active': False, 'speed_bps': 0.0} {'id': i, 'downloaded': 0, 'total': chunk_actual_sizes[i] if i < len(chunk_actual_sizes) else 0, 'active': False, 'speed_bps': 0.0}
for i in range(num_parts) for i in range(num_parts)
], ],
'lock': threading.Lock(), 'lock': threading.Lock()
'last_global_emit_time': [time.time()] # Shared mutable for global throttling timestamp
} }
chunk_futures = [] chunk_futures = []
@@ -200,8 +194,8 @@ def download_file_in_parts(file_url, save_path, total_size, num_parts, headers,
chunk_futures.append(chunk_pool.submit( chunk_futures.append(chunk_pool.submit(
_download_individual_chunk, chunk_url=file_url, temp_file_path=temp_file_path, _download_individual_chunk, chunk_url=file_url, temp_file_path=temp_file_path,
start_byte=start, end_byte=end, headers=headers, part_num=i, total_parts=num_parts, start_byte=start, end_byte=end, headers=headers, part_num=i, total_parts=num_parts,
progress_data=progress_data, cancellation_event=cancellation_event, skip_event=skip_event, global_emit_time_ref=progress_data['last_global_emit_time'], progress_data=progress_data, cancellation_event=cancellation_event, skip_event=skip_event,
pause_event=pause_event, cookies_for_chunk=cookies_for_chunk_session, logger_func=logger_func, emitter=emitter_for_multipart, logger_func=logger_func, emitter=emitter_for_multipart, # Pass emitter
api_original_filename=api_original_filename api_original_filename=api_original_filename
)) ))
@@ -215,8 +209,11 @@ def download_file_in_parts(file_url, save_path, total_size, num_parts, headers,
if cancellation_event and cancellation_event.is_set(): if cancellation_event and cancellation_event.is_set():
logger_func(f" Multi-part download for '{api_original_filename}' cancelled by main event.") logger_func(f" Multi-part download for '{api_original_filename}' cancelled by main event.")
all_chunks_successful = False all_chunks_successful = False
# Ensure a final progress update is sent with all chunks marked inactive (unless still active due to error)
if emitter_for_multipart: if emitter_for_multipart:
with progress_data['lock']: with progress_data['lock']:
# Ensure all chunks are marked inactive for the final signal if download didn't fully succeed or was cancelled
status_list_copy = [dict(s) for s in progress_data['chunks_status']] status_list_copy = [dict(s) for s in progress_data['chunks_status']]
if isinstance(emitter_for_multipart, queue.Queue): if isinstance(emitter_for_multipart, queue.Queue):
emitter_for_multipart.put({'type': 'file_progress', 'payload': (api_original_filename, status_list_copy)}) emitter_for_multipart.put({'type': 'file_progress', 'payload': (api_original_filename, status_list_copy)})
@@ -230,10 +227,12 @@ def download_file_in_parts(file_url, save_path, total_size, num_parts, headers,
for buf in iter(lambda: f_hash.read(4096*10), b''): # Read in larger buffers for hashing for buf in iter(lambda: f_hash.read(4096*10), b''): # Read in larger buffers for hashing
md5_hasher.update(buf) md5_hasher.update(buf)
calculated_hash = md5_hasher.hexdigest() calculated_hash = md5_hasher.hexdigest()
# Return an open file handle for the caller to manage (e.g., for compression)
# The caller is responsible for closing this handle and renaming/deleting the .part file.
return True, total_bytes_from_chunks, calculated_hash, open(temp_file_path, 'rb') return True, total_bytes_from_chunks, calculated_hash, open(temp_file_path, 'rb')
else: else:
logger_func(f" ❌ Multi-part download failed for '{api_original_filename}'. Success: {all_chunks_successful}, Bytes: {total_bytes_from_chunks}/{total_size}. Cleaning up.") logger_func(f" ❌ Multi-part download failed for '{api_original_filename}'. Success: {all_chunks_successful}, Bytes: {total_bytes_from_chunks}/{total_size}. Cleaning up.")
if os.path.exists(temp_file_path): if os.path.exists(temp_file_path):
try: os.remove(temp_file_path) try: os.remove(temp_file_path)
except OSError as e: logger_func(f" Failed to remove temp part file '{temp_file_path}': {e}") except OSError as e: logger_func(f" Failed to remove temp part file '{temp_file_path}': {e}")
return False, total_bytes_from_chunks, None, None return False, total_bytes_from_chunks, None, None

214
readme.md
View File

@@ -1,4 +1,4 @@
<h1 align="center">Kemono Downloader v4.1.1</h1> <h1 align="center">Kemono Downloader v3.4.0</h1>
<div align="center"> <div align="center">
<img src="https://github.com/Yuvi9587/Kemono-Downloader/blob/main/Read.png" alt="Kemono Downloader"/> <img src="https://github.com/Yuvi9587/Kemono-Downloader/blob/main/Read.png" alt="Kemono Downloader"/>
@@ -11,129 +11,11 @@ Built with **PyQt5**, this tool is ideal for users who want deep filtering, cust
--- ---
## What's New in v4.1.1? - Enhanced Image Discovery & Audio Filtering ## What's New in v3.4.0?
Version 4.1.1 brings significant enhancements, including smarter image capture from post content and a dedicated filter mode for audio files. This version brings significant enhancements to manga/comic downloading, filtering capabilities, and user experience:
### "Scan Content for Images" Feature ### 📖 Enhanced Manga/Comic Mode
- **Enhanced Image Discovery:** A new checkbox, "**Scan Content for Images**," has been added to the UI (grouped with "Download Thumbnails Only" and "Compress Large Images").
- **How it Works:**
- When enabled, the downloader meticulously scans the HTML content of each post's description or body.
- It searches for images in two main ways:
- **Directly linked absolute URLs** (e.g., `https://externalsite.com/image.png`) that end with a common image extension (jpg, png, gif, etc.).
- **Images embedded using HTML `<img>` tags.** The downloader extracts the `src` attribute from these tags and can resolve various path types:
- Absolute URLs (e.g., `http://...` or `https://...`)
- Protocol-relative URLs (e.g., `//cdn.example.com/image.jpg`)
- Root-relative paths (e.g., `/data/user_content/image.gif`), which are resolved against the site's base URL (like `https://kemono.su/data/user_content/image.gif`).
- This is particularly useful for capturing images that are part of the post's narrative but not formally listed in the API's file or attachment sections.
- **Default State:** This option is **unchecked by default**.
- **Key Interaction with "Download Thumbnails Only":** This new feature works closely with the existing "Download Thumbnails Only" option:
- If you enable "Download Thumbnails Only":
- The "Scan Content for Images" checkbox will **automatically become checked and disabled** (locked).
- The downloader then **exclusively downloads images discovered through the content scan**. Any API-listed thumbnails are bypassed, giving priority to images embedded directly in the post.
- If you disable "Download Thumbnails Only":
- The "Scan Content for Images" checkbox will become **enabled again and revert to being unchecked**. You can then manually enable it if you wish to scan content without being in thumbnail-only mode.
This feature ensures a more comprehensive download experience, especially for posts where images are integrated directly into the text.
### New "🎧 Only Audio" Filter Mode
Alongside image discovery, v4.1.1 also introduces/enhances a dedicated filter mode for audio enthusiasts:
- **Focused Audio Downloads:** The "🎧 Only Audio" option in the "Filter Files" radio button group allows you to download exclusively common audio file types. This includes formats like MP3, WAV, FLAC, M4A, OGG, and more.
- **Streamlined UI:** When "🎧 Only Audio" mode is active:
- Irrelevant UI options such as the "Skip Scope" button (for word-based post/file skipping) and the "Multi-part Download" toggle are hidden to simplify the interface.
- The "Show External Links in Log" checkbox is automatically disabled, as link extraction is not the focus of this mode.
- **Archive Handling:** Unlike the "📦 Only Archives" mode (which disables archive skipping), the "Skip .zip" and "Skip .rar" checkboxes remain enabled and configurable when "🎧 Only Audio" is selected. This gives you the flexibility to also exclude any archives encountered while in audio-only mode if desired.
- **Purpose:** This mode is perfect for users who primarily want to collect audio tracks, podcasts, or sound effects from posts without downloading other media types.
---
## Previous Update: What's New in v4.0.1?
Version 4.0.1 focuses on enhancing access to content and providing even smarter organization:
### Cookie Management
- **Access Content:** Seamlessly download from Kemono/Coomer as if you were logged in by using your browser's cookies.
- **Flexible Input:**
- Directly paste your cookie string (e.g., `name1=value1; name2=value2`).
- Browse and load cookies from a `cookies.txt` file (Netscape format).
- Automatic fallback to a `cookies.txt` file in the application directory if "Use Cookie" is enabled and no other source is specified.
- **Easy Activation:** A simple "Use Cookie" checkbox in the UI controls this feature.
- *Important Note: Cookie settings (text, file path, and enabled state) are configured per session and are not saved when the application is closed. You will need to re-apply them on each launch if needed.*
---
### Advanced `Known.txt` and Character Filtering
The `Known.txt` system has been revamped for improved performance and stability. The previous method of handling known names could become resource-intensive with large lists, potentially leading to application slowdowns or crashes. This new, streamlined system offers more direct control and robust organization.
The `Known.txt` file and the "Filter by Character(s)" input field work together to provide powerful and flexible content organization. The `Known.txt` file itself has a straightforward syntax, while the UI input allows for more complex session-specific grouping and alias definitions that can then be added to `Known.txt`.
**1. `Known.txt` File Syntax (Located in App Directory):**
`Known.txt` stores your persistent list of characters, series, or keywords for folder organization. Each line is an entry:
- **Simple Entries:**
- A line like `My Awesome Series` or `Nami`.
- **Behavior:** Content matching this term will be saved into a folder named "My Awesome Series" or "Nami" respectively (if "Separate Folders" is enabled).
**2. "Filter by Character(s)" UI Input Field:**
This field allows for dynamic filtering for the current download session and provides options for how new entries are added to `Known.txt`.
- **Standard Names:**
- Input: `Nami, Robin`
- Session Behavior: Filters for "Nami" OR "Robin". If "Separate Folders" is on, creates folders "Nami" and "Robin".
- `Known.txt` Addition: If "Nami" is new and selected for addition in the confirmation dialog, it's added as `Nami` on a new line in `Known.txt`.
- **Grouped Aliases for a Single Character (using `(...)~` syntax):**
- Input: `(Boa, Hancock)~`
- Meaning: "Boa" and "Hancock" are different names/aliases for the *same character*. The names are listed within parentheses separated by commas (e.g., `name1, alias1, alias2`), and the entire group is followed by a `~` symbol. This is useful when a creator uses different names for the same character.
- Session Behavior: Filters for "Boa" OR "Hancock". If "Separate Folders" is on, creates a single folder named "Boa Hancock".
- `Known.txt` Addition: If this group is new and selected for addition, it's added to `Known.txt` as a grouped alias entry, typically `(Boa Hancock)`. The first name in the `Known.txt` entry (e.g., "Boa Hancock") becomes the primary folder name.
- **Combined Folder for Distinct Characters (using `(...)` syntax):**
- Input: `(Vivi, Uta)`
- Meaning: "Vivi" and "Uta" are *distinct characters*, but for this download session, their content should be grouped into a single folder. The names are listed within parentheses separated by commas. This is useful for grouping art of less frequent characters without creating many small individual folders.
- Session Behavior: Filters for "Vivi" OR "Uta". If "Separate Folders" is on, creates a single folder named "Vivi Uta".
- `Known.txt` Addition: If this "combined group" is new and selected for addition, "Vivi" and "Uta" are added to `Known.txt` as *separate, individual simple entries* on new lines:
```
Vivi
Uta
```
The combined folder "Vivi Uta" is a session-only convenience; `Known.txt` stores them as distinct entities for future individual use.
**3. Interaction with `Known.txt`:**
- **Adding New Names from Filters:** When you use the "Filter by Character(s)" input, if any names or groups are new (not already in `Known.txt`), a dialog will appear after you start the download. This dialog allows you to select which of these new names/groups should be added to `Known.txt`, formatted according to the rules described above.
- **Intelligent Fallback:** If "Separate Folders by Name/Title" is active, and content doesn't match the "Filter by Character(s)" UI input, the downloader consults your `Known.txt` file for folder naming.
- **Direct Management:** You can add simple entries directly to `Known.txt` using the list and "Add" button in the UI's `Known.txt` management section. For creating or modifying complex grouped alias entries directly in the file, or for bulk edits, click the "Open Known.txt" button. The application reloads `Known.txt` on startup or before a download process begins.
- **Using Known Names to Populate Filters (via "Add to Filter" Button):**
- Next to the "Add" button in the `Known.txt` management section, a "⤵️ Add to Filter" button provides a quick way to use your existing known names.
- Clicking this opens a popup window displaying all entries from your `Known.txt` file, each with a checkbox.
- The popup includes:
- A search bar to quickly filter the list of names.
- "Select All" and "Deselect All" buttons for convenience.
- After selecting the desired names, click "Add Selected".
- The chosen names will be inserted into the "Filter by Character(s)" input field.
- **Important Formatting:** If a selected entry from `Known.txt` is a group (e.g., originally `(Boa Hancock)` in `Known.txt`, which implies aliases "Boa" and "Hancock"), it will be added to the filter field as `(Boa, Hancock)~`. Simple names are added as-is.
---
## What's in v3.5.0? (Previous Update)
This version brought significant enhancements to manga/comic downloading, filtering capabilities, and user experience:
### Enhanced Manga/Comic Mode
- **Optional Filename Prefix:**
- When using the "Date Based" or "Original File Name" manga styles, an optional prefix can be specified in the UI.
- This prefix will be prepended to each filename generated by these styles.
- **Example (Date Based):** If prefix is `MySeries_`, files become `MySeries_001.jpg`, `MySeries_002.png`, etc.
- **Example (Original File Name):** If prefix is `Comic_Vol1_`, an original file `page_01.jpg` becomes `Comic_Vol1_page_01.jpg`.
- This input field appears automatically when either of these two manga naming styles is selected.
- **New "Date Based" Filename Style:** - **New "Date Based" Filename Style:**
@@ -144,17 +26,10 @@ This version brought significant enhancements to manga/comic downloading, filter
- **Guaranteed Order:** Disables multi-threading for post processing to ensure sequential accuracy. - **Guaranteed Order:** Disables multi-threading for post processing to ensure sequential accuracy.
- Works alongside the existing "Post Title" and "Original File Name" styles. - Works alongside the existing "Post Title" and "Original File Name" styles.
- **New "Title+G.Num (Post Title + Global Numbering)" Filename Style:**
- Ideal for series where you want each file to be prefixed by its post title but still maintain a global sequential number across all posts from a single download session.
- **Naming Convention:** Files are named using the cleaned post title as a prefix, followed by an underscore and a globally incrementing number (e.g., `Post Title_001.ext`, `Post Title_002.ext`).
- **Example:**
- Post "Chapter 1: The Adventure Begins" (contains 2 files: `imageA.jpg`, `imageB.png`) -> `Chapter 1 The Adventure Begins_001.jpg`, `Chapter 1 The Adventure Begins_002.png`
- Next Post "Chapter 2: New Friends" (contains 1 file: `cover.jpg`) -> `Chapter 2 New Friends_003.jpg`
- **Sequential Integrity:** Multithreading for post processing is automatically disabled when this style is selected to ensure the global numbering is strictly sequential.
--- ---
### "Remove Words from Filename" Feature ### ✂️ "Remove Words from Filename" Feature
- Specify comma-separated words or phrases (case-insensitive) that will be automatically removed from filenames. - Specify comma-separated words or phrases (case-insensitive) that will be automatically removed from filenames.
@@ -162,7 +37,7 @@ This version brought significant enhancements to manga/comic downloading, filter
--- ---
### New "Only Archives" File Filter Mode ### 📦 New "Only Archives" File Filter Mode
- Exclusively downloads `.zip` and `.rar` files. - Exclusively downloads `.zip` and `.rar` files.
@@ -170,7 +45,7 @@ This version brought significant enhancements to manga/comic downloading, filter
--- ---
### Improved Character Filter Scope - "Comments (Beta)" ### 🗣️ Improved Character Filter Scope - "Comments (Beta)"
- **File-First Check:** Prioritizes matching filenames before checking post comments for character names. - **File-First Check:** Prioritizes matching filenames before checking post comments for character names.
@@ -178,7 +53,7 @@ This version brought significant enhancements to manga/comic downloading, filter
--- ---
### Refined "Missed Character Log" ### 🧐 Refined "Missed Character Log"
- Displays a capitalized, alphabetized list of key terms from skipped post titles. - Displays a capitalized, alphabetized list of key terms from skipped post titles.
@@ -186,25 +61,25 @@ This version brought significant enhancements to manga/comic downloading, filter
--- ---
### Enhanced Multi-part Download Progress ### 🚀 Enhanced Multi-part Download Progress
- Granular visibility into active chunk downloads and combined speed for large files. - Granular visibility into active chunk downloads and combined speed for large files.
--- ---
### Updated Onboarding Tour ### 🗺️ Updated Onboarding Tour
- Improved guide for new users, covering v4.0.0 features and existing core functions. - Improved guide for new users, covering v3.4.0 features and existing core functions.
--- ---
### Robust Configuration Path ### 🛡️ Robust Configuration Path
- Settings and `Known.txt` are now stored in the same folder as app. - Settings and `Known.txt` are now stored in the system-standard application data folder (e.g., `AppData`, `~/.local/share`).
--- ---
## Core Features ## 🖥️ Core Features
--- ---
@@ -224,17 +99,11 @@ This version brought significant enhancements to manga/comic downloading, filter
--- ---
### Smart Filtering ### 🧠 Smart Filtering
- **Character Name Filtering:** - **Character Name Filtering:**
- Use `Tifa, Aerith` or group `(Boa, Hancock)` → folder `Boa Hancock` - Use `Tifa, Aerith` or group `(Boa, Hancock)` → folder `Boa Hancock`
- Flexible input for current session and for adding to `Known.txt`.
- Examples:
- `Nami` (simple character)
- `(Boa Hancock)~` (aliases for one character, session folder "Boa Hancock", adds `(Boa Hancock)` to `Known.txt`)
- `(Vivi, Uta)` (distinct characters, session folder "Vivi Uta", adds `Vivi` and `Uta` separately to `Known.txt`)
- A "⤵️ Add to Filter" button (near the `Known.txt` management UI) allows you to quickly populate this field by selecting from your existing `Known.txt` entries via a popup with search and checkbox selection.
- See "Advanced `Known.txt` and Character Filtering" for full details.
- **Filter Scopes:** - **Filter Scopes:**
- `Files` - `Files`
- `Title` - `Title`
@@ -257,7 +126,7 @@ This version brought significant enhancements to manga/comic downloading, filter
--- ---
### Manga/Comic Mode (Creator Feeds Only) ### 📚 Manga/Comic Mode (Creator Feeds Only)
- **Chronological Processing** — Oldest posts first - **Chronological Processing** — Oldest posts first
@@ -265,13 +134,12 @@ This version brought significant enhancements to manga/comic downloading, filter
- `Name: Post Title (Default)` - `Name: Post Title (Default)`
- `Name: Original File` - `Name: Original File`
- `Name: Date Based (New)` - `Name: Date Based (New)`
- `Name: Title+G.Num (Post Title + Global Numbering)`
- **Best With:** Character filters set to manga/series title - **Best With:** Character filters set to manga/series title
--- ---
### Folder Structure & Naming ### 📁 Folder Structure & Naming
- **Subfolders:** - **Subfolders:**
- Auto-created based on character name, post title, or `Known.txt` - Auto-created based on character name, post title, or `Known.txt`
@@ -282,21 +150,16 @@ This version brought significant enhancements to manga/comic downloading, filter
--- ---
### Thumbnail & Compression Tools ### 🖼️ Thumbnail & Compression Tools
- **Download Thumbnails Only:**
- Downloads small preview images from the API instead of full-sized files (if available). - **Download Thumbnails Only**
- **Interaction with "Scan Content for Images" (New in v4.1.1):** When "Download Thumbnails Only" is active, "Scan Content for Images" is auto-enabled, and only images found by the content scan are downloaded. See "What's New in v4.1.1" for details.
- **Scan Content for Images (New in v4.1.1):**
- A UI option to scan the HTML content of posts for embedded image URLs (from `<img>` tags or direct links).
- Resolves relative paths and helps capture images not listed in the API's formal attachments.
- See the "What's New in v4.1.1?" section for a comprehensive explanation.
- **Compress to WebP** (via Pillow) - **Compress to WebP** (via Pillow)
- Converts large images to smaller WebP versions - Converts large images to smaller WebP versions
--- ---
### Performance Features ### ⚙️ Performance Features
- **Multithreading:** - **Multithreading:**
- For both post processing and file downloading - For both post processing and file downloading
@@ -308,7 +171,7 @@ This version brought significant enhancements to manga/comic downloading, filter
--- ---
### Logging & Progress ### 📋 Logging & Progress
- **Real-time Logs:** Activity, errors, skipped posts - **Real-time Logs:** Activity, errors, skipped posts
@@ -320,22 +183,19 @@ This version brought significant enhancements to manga/comic downloading, filter
--- ---
### Config System ### 🗃️ Config System
- **`Known.txt` for Smart Folder Naming (Located in App Directory):** - **Known.txt:**
- A user-editable file that stores a list of preferred names, series titles, or keywords. - Stores names for smart folder suggestions
- It's primarily used as an intelligent fallback for folder creation when "Separate Folders by Name/Title" is enabled. - Supports aliases via `(alias1, alias2)`
- **Syntax:**
- Simple entries: `My Favorite Series` (creates folder "My Favorite Series", matches "My Favorite Series").
- Grouped entries: `(Desired Folder Name, alias1, alias2)` (creates folder "Desired Folder Name"; matches "Desired Folder Name", "alias1", or "alias2").
- **Settings Stored in App Directory** - **Stored in Standard App Data Path**
- **Editable Within GUI** - **Editable Within GUI**
--- ---
## Installation ## 💻 Installation
--- ---
@@ -355,7 +215,7 @@ pip install PyQt5 requests Pillow
*** ***
## ** Build a Standalone Executable (Optional)** ## **🛠️ Build a Standalone Executable (Optional)**
1. Install PyInstaller: 1. Install PyInstaller:
```bash ```bash
@@ -371,14 +231,14 @@ pyinstaller --name "Kemono Downloader" --onefile --windowed --icon="Kemono.ico"
*** ***
## ** Config Files** ## **🗂 Config Files**
- `settings.json` — Stores your UI preferences and settings.
- `Known.txt` — Stores character names, series titles, or keywords for organizing downloaded content into specific folders. - `Known.txt` — character/show names used for folder organization
- Supports simple entries (e.g., `My Series`) and grouped entries for aliases (e.g., `(Folder Name, alias1, alias2)` where "Folder Name" is the name of the created folder, and all terms are used for matching). - Supports grouped names in format: `(Name1, Name2)`
*** ***
## ** Feedback & Support** ## **💬 Feedback & Support**
Issues? Suggestions? Issues? Suggestions?
Open an issue on the [GitHub repository](https://github.com/Yuvi9587/kemono-downloader) or join our community. Open an issue on the [GitHub repository](https://github.com/Yuvi9587/kemono-downloader) or join our community.