mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-29 16:14:44 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
651f9d9f8d | ||
|
|
decef6730f | ||
|
|
32a12e8a09 | ||
|
|
62007d2d45 | ||
|
|
f1e592cf99 | ||
|
|
bf111d109a | ||
|
|
00f8ff63d6 | ||
|
|
aee0ff999d | ||
|
|
b5e9080285 | ||
|
|
25d33f1531 | ||
|
|
ff0ccb2631 | ||
|
|
da507b2b3a | ||
|
|
9165903e96 | ||
|
|
f85de58fcb | ||
|
|
ccfb8496a2 | ||
|
|
e0d3e1b5af | ||
|
|
50ee50cd5c | ||
|
|
8982026d79 | ||
|
|
aec44f1782 | ||
|
|
866a5a90de | ||
|
|
929051d46c | ||
|
|
eada5057b7 | ||
|
|
fe0b369446 | ||
|
|
c0c2db709b |
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 [Yuvi9587]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
2018
downloader_utils.py
2018
downloader_utils.py
File diff suppressed because it is too large
Load Diff
238
multipart_downloader.py
Normal file
238
multipart_downloader.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
import http.client
|
||||||
|
import traceback
|
||||||
|
import threading
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
CHUNK_DOWNLOAD_RETRY_DELAY = 2 # Slightly reduced for faster retries if needed
|
||||||
|
MAX_CHUNK_DOWNLOAD_RETRIES = 1 # Further reduced for quicker fallback if a chunk is problematic
|
||||||
|
DOWNLOAD_CHUNK_SIZE_ITER = 1024 * 256 # 256KB for iter_content within a chunk download
|
||||||
|
|
||||||
|
|
||||||
|
def _download_individual_chunk(chunk_url, temp_file_path, start_byte, end_byte, headers,
|
||||||
|
part_num, total_parts, progress_data, cancellation_event, skip_event,
|
||||||
|
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."""
|
||||||
|
if cancellation_event and cancellation_event.is_set():
|
||||||
|
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Download cancelled before start.")
|
||||||
|
return 0, False # bytes_downloaded, success
|
||||||
|
if skip_event and skip_event.is_set():
|
||||||
|
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Skip event triggered before start.")
|
||||||
|
return 0, False
|
||||||
|
|
||||||
|
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
|
||||||
|
chunk_headers['Range'] = f"bytes={start_byte}-{end_byte}"
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
bytes_this_chunk = 0
|
||||||
|
last_progress_emit_time_for_chunk = time.time()
|
||||||
|
last_speed_calc_time = time.time()
|
||||||
|
bytes_at_last_speed_calc = 0
|
||||||
|
|
||||||
|
for attempt in range(MAX_CHUNK_DOWNLOAD_RETRIES + 1):
|
||||||
|
if cancellation_event and cancellation_event.is_set():
|
||||||
|
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Cancelled during retry loop.")
|
||||||
|
return bytes_this_chunk, False
|
||||||
|
if skip_event and skip_event.is_set():
|
||||||
|
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Skip event during retry loop.")
|
||||||
|
return bytes_this_chunk, False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if attempt > 0:
|
||||||
|
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)))
|
||||||
|
# Reset speed calculation on retry
|
||||||
|
last_speed_calc_time = time.time()
|
||||||
|
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'}"
|
||||||
|
logger_func(log_msg)
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Confirmed 0-byte file.")
|
||||||
|
with progress_data['lock']:
|
||||||
|
progress_data['chunks_status'][part_num]['active'] = False
|
||||||
|
progress_data['chunks_status'][part_num]['speed_bps'] = 0
|
||||||
|
return 0, True
|
||||||
|
|
||||||
|
with open(temp_file_path, 'r+b') as f: # Open in read-write binary
|
||||||
|
f.seek(start_byte)
|
||||||
|
for data_segment in response.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE_ITER):
|
||||||
|
if cancellation_event and cancellation_event.is_set():
|
||||||
|
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Cancelled during data iteration.")
|
||||||
|
return bytes_this_chunk, False
|
||||||
|
if skip_event and skip_event.is_set():
|
||||||
|
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Skip event during data iteration.")
|
||||||
|
return bytes_this_chunk, False
|
||||||
|
if data_segment:
|
||||||
|
f.write(data_segment)
|
||||||
|
bytes_this_chunk += len(data_segment)
|
||||||
|
|
||||||
|
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['chunks_status'][part_num]['downloaded'] = bytes_this_chunk
|
||||||
|
progress_data['chunks_status'][part_num]['active'] = True
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
time_delta_speed = current_time - last_speed_calc_time
|
||||||
|
if time_delta_speed > 0.5: # Calculate speed every 0.5 seconds
|
||||||
|
bytes_delta = bytes_this_chunk - bytes_at_last_speed_calc
|
||||||
|
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
|
||||||
|
last_speed_calc_time = current_time
|
||||||
|
bytes_at_last_speed_calc = bytes_this_chunk
|
||||||
|
|
||||||
|
# Emit progress more frequently from within the chunk download
|
||||||
|
if current_time - last_progress_emit_time_for_chunk > 0.1: # Emit up to 10 times/sec per chunk
|
||||||
|
if emitter:
|
||||||
|
# Ensure we read the latest total downloaded from progress_data
|
||||||
|
# Send a copy of the chunks_status list
|
||||||
|
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
|
||||||
|
|
||||||
|
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout, http.client.IncompleteRead) as e:
|
||||||
|
logger_func(f" ❌ [Chunk {part_num + 1}/{total_parts}] Retryable error: {e}")
|
||||||
|
if attempt == MAX_CHUNK_DOWNLOAD_RETRIES:
|
||||||
|
logger_func(f" ❌ [Chunk {part_num + 1}/{total_parts}] Failed after {MAX_CHUNK_DOWNLOAD_RETRIES} retries.")
|
||||||
|
return bytes_this_chunk, False
|
||||||
|
except requests.exceptions.RequestException as e: # Includes 4xx/5xx errors after raise_for_status
|
||||||
|
logger_func(f" ❌ [Chunk {part_num + 1}/{total_parts}] Non-retryable error: {e}")
|
||||||
|
return bytes_this_chunk, False
|
||||||
|
except Exception as e:
|
||||||
|
logger_func(f" ❌ [Chunk {part_num + 1}/{total_parts}] Unexpected error: {e}\n{traceback.format_exc(limit=1)}")
|
||||||
|
return bytes_this_chunk, False
|
||||||
|
|
||||||
|
# Ensure final status is marked as inactive if loop finishes due to retries
|
||||||
|
with progress_data['lock']:
|
||||||
|
progress_data['chunks_status'][part_num]['active'] = False
|
||||||
|
progress_data['chunks_status'][part_num]['speed_bps'] = 0
|
||||||
|
return bytes_this_chunk, False # Should be unreachable
|
||||||
|
|
||||||
|
|
||||||
|
def download_file_in_parts(file_url, save_path, total_size, num_parts, headers, api_original_filename,
|
||||||
|
emitter_for_multipart, cancellation_event, skip_event, logger_func): # Renamed signals, logger
|
||||||
|
"""
|
||||||
|
Downloads a file in multiple parts concurrently.
|
||||||
|
Returns: (download_successful_flag, downloaded_bytes, calculated_file_hash, temp_file_handle_or_None)
|
||||||
|
The temp_file_handle will be an open read-binary file handle to the .part file if successful, otherwise None.
|
||||||
|
It is the responsibility of the caller to close this handle and rename/delete the .part file.
|
||||||
|
"""
|
||||||
|
logger_func(f"⬇️ Initializing Multi-part Download ({num_parts} parts) for: '{api_original_filename}' (Size: {total_size / (1024*1024):.2f} MB)")
|
||||||
|
temp_file_path = save_path + ".part"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(temp_file_path, 'wb') as f_temp:
|
||||||
|
if total_size > 0:
|
||||||
|
f_temp.truncate(total_size) # Pre-allocate space
|
||||||
|
except IOError as e:
|
||||||
|
logger_func(f" ❌ Error creating/truncating temp file '{temp_file_path}': {e}")
|
||||||
|
return False, 0, None, None
|
||||||
|
|
||||||
|
chunk_size_calc = total_size // num_parts
|
||||||
|
chunks_ranges = []
|
||||||
|
for i in range(num_parts):
|
||||||
|
start = i * chunk_size_calc
|
||||||
|
end = start + chunk_size_calc - 1 if i < num_parts - 1 else total_size - 1
|
||||||
|
if start <= end: # Valid range
|
||||||
|
chunks_ranges.append((start, end))
|
||||||
|
elif total_size == 0 and i == 0: # Special case for 0-byte file
|
||||||
|
chunks_ranges.append((0, -1)) # Indicates 0-byte file, download 0 bytes from offset 0
|
||||||
|
|
||||||
|
chunk_actual_sizes = []
|
||||||
|
for start, end in chunks_ranges:
|
||||||
|
if end == -1 and start == 0: # 0-byte file
|
||||||
|
chunk_actual_sizes.append(0)
|
||||||
|
else:
|
||||||
|
chunk_actual_sizes.append(end - start + 1)
|
||||||
|
|
||||||
|
if not chunks_ranges and total_size > 0:
|
||||||
|
logger_func(f" ⚠️ No valid chunk ranges for multipart download of '{api_original_filename}'. Aborting multipart.")
|
||||||
|
if os.path.exists(temp_file_path): os.remove(temp_file_path)
|
||||||
|
return False, 0, None, None
|
||||||
|
|
||||||
|
progress_data = {
|
||||||
|
'total_file_size': total_size, # Overall file size for reference
|
||||||
|
'total_downloaded_so_far': 0, # New key for overall progress
|
||||||
|
'chunks_status': [ # Status for each chunk
|
||||||
|
{'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)
|
||||||
|
],
|
||||||
|
'lock': threading.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk_futures = []
|
||||||
|
all_chunks_successful = True
|
||||||
|
total_bytes_from_chunks = 0 # Still useful to verify total downloaded against file size
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=num_parts, thread_name_prefix=f"MPChunk_{api_original_filename[:10]}_") as chunk_pool:
|
||||||
|
for i, (start, end) in enumerate(chunks_ranges):
|
||||||
|
if cancellation_event and cancellation_event.is_set(): all_chunks_successful = False; break
|
||||||
|
chunk_futures.append(chunk_pool.submit(
|
||||||
|
_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,
|
||||||
|
progress_data=progress_data, cancellation_event=cancellation_event, skip_event=skip_event,
|
||||||
|
logger_func=logger_func, emitter=emitter_for_multipart, # Pass emitter
|
||||||
|
api_original_filename=api_original_filename
|
||||||
|
))
|
||||||
|
|
||||||
|
for future in as_completed(chunk_futures):
|
||||||
|
if cancellation_event and cancellation_event.is_set(): all_chunks_successful = False; break
|
||||||
|
bytes_downloaded_this_chunk, success_this_chunk = future.result()
|
||||||
|
total_bytes_from_chunks += bytes_downloaded_this_chunk
|
||||||
|
if not success_this_chunk:
|
||||||
|
all_chunks_successful = False
|
||||||
|
|
||||||
|
if cancellation_event and cancellation_event.is_set():
|
||||||
|
logger_func(f" Multi-part download for '{api_original_filename}' cancelled by main event.")
|
||||||
|
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:
|
||||||
|
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']]
|
||||||
|
if isinstance(emitter_for_multipart, queue.Queue):
|
||||||
|
emitter_for_multipart.put({'type': 'file_progress', 'payload': (api_original_filename, status_list_copy)})
|
||||||
|
elif hasattr(emitter_for_multipart, 'file_progress_signal'): # PostProcessorSignals-like
|
||||||
|
emitter_for_multipart.file_progress_signal.emit(api_original_filename, status_list_copy)
|
||||||
|
|
||||||
|
if all_chunks_successful and (total_bytes_from_chunks == total_size or total_size == 0):
|
||||||
|
logger_func(f" ✅ Multi-part download successful for '{api_original_filename}'. Total bytes: {total_bytes_from_chunks}")
|
||||||
|
md5_hasher = hashlib.md5()
|
||||||
|
with open(temp_file_path, 'rb') as f_hash:
|
||||||
|
for buf in iter(lambda: f_hash.read(4096*10), b''): # Read in larger buffers for hashing
|
||||||
|
md5_hasher.update(buf)
|
||||||
|
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')
|
||||||
|
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.")
|
||||||
|
if os.path.exists(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}")
|
||||||
|
return False, total_bytes_from_chunks, None, None
|
||||||
308
readme.md
308
readme.md
@@ -1,108 +1,240 @@
|
|||||||
# Kemono Downloader
|
# Kemono Downloader v3.4.0
|
||||||
|
|
||||||
A simple, multi-platform GUI application built with PyQt5 to download content from Kemono.su or Coomer.party creator pages or specific posts, with options for filtering and organizing downloads.
|
A powerful, feature-rich GUI application for downloading content from **[Kemono.su](https://kemono.su)** and **[Coomer.party](https://coomer.party)**.
|
||||||
|
Built with **PyQt5**, this tool is ideal for users who want deep filtering, customizable folder structures, efficient downloads, and intelligent automation — all within a modern, user-friendly graphical interface.
|
||||||
|
|
||||||
## Features
|
---
|
||||||
|
|
||||||
* **GUI Interface:** Easy-to-use graphical interface.
|
## ✨ What's New in v3.4.0?
|
||||||
* **URL Support:** Download from a creator's main page (paginated) or a specific post URL from Kemono or Coomer sites.
|
|
||||||
* **Download Location:** Select your desired output directory.
|
|
||||||
* **Subfolder Organization:**
|
|
||||||
* Organize downloads into folders based on character/artist names found in post titles (using your "Known Names" list).
|
|
||||||
* Option to create a custom folder for single post downloads.
|
|
||||||
* Automatic folder naming based on post title if no known names are matched.
|
|
||||||
* **Known Names List:** Manage a persistent list of known names (artists, characters, series) for improved folder organization and filtering.
|
|
||||||
* **Content Filtering:**
|
|
||||||
* **Character/Name Filter:** Only download posts where the specified known name is found in the title.
|
|
||||||
* **File Type Filter:** Download All Files, Images/GIFs Only, or Videos Only.
|
|
||||||
* **Skip Words Filter:** Specify a list of comma-separated words to skip posts or files if these words appear in their titles or filenames.
|
|
||||||
* **Archive Skipping:** Options to skip `.zip` and `.rar` files (enabled by default).
|
|
||||||
* **Image Compression:** Optionally compress large images (larger than 1.5MB) to WebP format to save space (requires Pillow library).
|
|
||||||
* **Thumbnail Downloading:** Option to download thumbnails. (Note: The previous local API method for enhanced thumbnail fetching has been removed. Thumbnail availability might depend on the source.)
|
|
||||||
* **Duplicate Prevention:**
|
|
||||||
* Avoids re-downloading files with the same content hash.
|
|
||||||
* Checks for existing filenames in the target directory.
|
|
||||||
* **Multithreading:** Utilizes multithreading for faster downloads from full creator pages (single posts are processed in a single thread).
|
|
||||||
* **Progress Log:** View detailed download progress, status messages, and errors.
|
|
||||||
* **Dark Theme:** Built-in dark theme for comfortable use.
|
|
||||||
* **Download Management:**
|
|
||||||
* Ability to cancel an ongoing download process.
|
|
||||||
* Option to skip the specific file currently being downloaded (in single-thread mode).
|
|
||||||
* **Persistent Configuration:** Saves the "Known Names" list to a local file.
|
|
||||||
|
|
||||||
## Prerequisites
|
This version brings significant enhancements to manga/comic downloading, filtering capabilities, and user experience:
|
||||||
|
|
||||||
* Python 3.6 or higher
|
---
|
||||||
* `pip` package installer
|
|
||||||
|
|
||||||
## Installation
|
### 📖 Enhanced Manga/Comic Mode
|
||||||
|
|
||||||
1. Clone or download this repository/script to your local machine.
|
- **New "Date Based" Filename Style:**
|
||||||
2. Navigate to the script's directory in your terminal or command prompt.
|
|
||||||
3. Install the required Python libraries:
|
|
||||||
```bash
|
|
||||||
pip install PyQt5 requests Pillow
|
|
||||||
```
|
|
||||||
*(Pillow is required for image compression and potentially for basic image handling.)*
|
|
||||||
|
|
||||||
## How to Run
|
- Perfect for truly sequential content! Files are named numerically (e.g., `001.jpg`, `002.jpg`, `003.ext`...) across an *entire creator's feed*, strictly following post publication order.
|
||||||
|
|
||||||
1. Make sure you have followed the installation steps.
|
- **Smart Numbering:** Automatically resumes from the highest existing number found in the series folder (and subfolders, if "Subfolder per Post" is enabled).
|
||||||
2. Open your terminal or command prompt and navigate to the script's directory.
|
|
||||||
3. Run the script using Python:
|
|
||||||
```bash
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to Use
|
- **Guaranteed Order:** Disables multi-threading for post processing to ensure sequential accuracy.
|
||||||
|
|
||||||
1. **URL Input:** Enter the URL of the Kemono/Coomer creator page (e.g., `https://kemono.su/patreon/user/12345`) or a specific post (e.g., `https://kemono.su/patreon/user/12345/post/67890`) into the "Kemono Creator/Post URL" field.
|
- Works alongside the existing "Post Title" and "Original File Name" styles.
|
||||||
2. **Download Location:** Use the "Browse" button to select the root directory where you want to save the downloaded content.
|
|
||||||
3. **Custom Folder Name (Single Post Only):** If downloading a single post and "Separate Folders" is enabled, you can specify a custom folder name for that post's content.
|
|
||||||
4. **Filter by Show/Character Name (Optional):** If "Separate Folders" is enabled, enter a name from your "Known Names" list. Only posts with titles matching this name will be downloaded into a folder named accordingly. If empty, the script will try to match any known name or derive a folder name from the post title.
|
|
||||||
5. **Skip Posts/Files with Words:** Enter comma-separated words (e.g., `WIP, sketch, preview`). Posts or files containing these words in their title/filename will be skipped.
|
|
||||||
6. **File Type Filter:**
|
|
||||||
* **All:** Downloads all files.
|
|
||||||
* **Images/GIFs:** Downloads common image formats and GIFs.
|
|
||||||
* **Videos:** Downloads common video formats.
|
|
||||||
7. **Options (Checkboxes):**
|
|
||||||
* **Separate Folders by Name/Title:** Enables creation of subfolders based on known names or post titles. Controls visibility of "Filter by Show/Character Name" and "Custom Folder Name". (Default: On)
|
|
||||||
* **Download Thumbnails Only:** Attempts to download only thumbnails for posts. (Default: Off)
|
|
||||||
* **Skip .zip / Skip .rar:** Prevents downloading of these archive types. (Default: On)
|
|
||||||
* **Compress Large Images (to WebP):** Compresses images larger than 1.5MB. (Default: Off)
|
|
||||||
* **Use Multithreading:** Enables faster downloads for full creator pages. (Default: On)
|
|
||||||
8. **Known Names List:**
|
|
||||||
* The list on the left ("Known Shows/Characters") displays names used for folder organization and filtering. This list is saved in `Known.txt`.
|
|
||||||
* Use the input field below the list and the "➕ Add" button to add new names.
|
|
||||||
* Select names and click "🗑️ Delete Selected" to remove them.
|
|
||||||
* A search bar above the list allows you to filter the displayed names.
|
|
||||||
9. **Start Download:** Click "⬇️ Start Download" to begin.
|
|
||||||
10. **Cancel / Skip:**
|
|
||||||
* **❌ Cancel:** Stops the entire download process.
|
|
||||||
* **⏭️ Skip Current File:** (Only in single-thread mode during file download) Skips the currently downloading file and moves to the next.
|
|
||||||
11. **Progress Log:** The area on the right shows detailed logs of the download process, including fetched posts, saved files, skips, and errors.
|
|
||||||
|
|
||||||
## Building an Executable (Optional)
|
---
|
||||||
|
|
||||||
You can create a standalone `.exe` file for Windows using `PyInstaller`.
|
### ✂️ "Remove Words from Filename" Feature
|
||||||
|
|
||||||
1. Install PyInstaller: `pip install pyinstaller`
|
- Specify comma-separated words or phrases (case-insensitive) that will be automatically removed from filenames.
|
||||||
2. Obtain an icon file (`.ico`). Place it in the same directory as `main.py`.
|
|
||||||
3. Open your terminal in the script's directory and run:
|
|
||||||
```bash
|
|
||||||
pyinstaller --name "YourAppName" --onefile --windowed --icon="your_icon.ico" main.py
|
|
||||||
```
|
|
||||||
Replace `"YourAppName"` with your desired application name and `"your_icon.ico"` with the actual name of your icon file.
|
|
||||||
4. The executable will be found in the `./dist` folder.
|
|
||||||
|
|
||||||
## Configuration
|
- Example: `patreon, [HD], _final` transforms `AwesomeArt_patreon_[HD]_final.jpg` into `AwesomeArt.jpg`.
|
||||||
|
|
||||||
The application saves your list of known names (characters, artists, series, etc.) to a file named `Known.txt` in the same directory as the script (`main.py`). Each name is stored on a new line. You can manually edit this file if needed.
|
---
|
||||||
|
|
||||||
## Dark Theme
|
### 📦 New "Only Archives" File Filter Mode
|
||||||
|
|
||||||
The application uses a built-in dark theme for the user interface.
|
- Exclusively downloads `.zip` and `.rar` files.
|
||||||
|
|
||||||
## Contributing
|
- Automatically disables conflicting options like "Skip .zip/.rar" and external link logging.
|
||||||
|
|
||||||
Contributions are welcome! If you find a bug or have a feature request, please open an issue on the GitHub repository (if applicable). If you want to contribute code, please fork the repository and create a pull request.
|
---
|
||||||
|
|
||||||
|
### 🗣️ Improved Character Filter Scope - "Comments (Beta)"
|
||||||
|
|
||||||
|
- **File-First Check:** Prioritizes matching filenames before checking post comments for character names.
|
||||||
|
|
||||||
|
- **Comment Fallback:** Only checks comments if no filename match is found, reducing unnecessary API calls.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🧐 Refined "Missed Character Log"
|
||||||
|
|
||||||
|
- Displays a capitalized, alphabetized list of key terms from skipped post titles.
|
||||||
|
|
||||||
|
- Makes it easier to spot patterns or characters that might be unintentionally excluded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🚀 Enhanced Multi-part Download Progress
|
||||||
|
|
||||||
|
- Granular visibility into active chunk downloads and combined speed for large files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🗺️ Updated Onboarding Tour
|
||||||
|
|
||||||
|
- Improved guide for new users, covering v3.4.0 features and existing core functions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🛡️ Robust Configuration Path
|
||||||
|
|
||||||
|
- Settings and `Known.txt` are now stored in the system-standard application data folder (e.g., `AppData`, `~/.local/share`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥️ Core Features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Interface & Workflow
|
||||||
|
|
||||||
|
- **Clean PyQt5 GUI** — Simple, modern, and dark-themed.
|
||||||
|
|
||||||
|
- **Persistent Settings** — Saves preferences between sessions.
|
||||||
|
|
||||||
|
- **Download Modes:**
|
||||||
|
- Single Post URL
|
||||||
|
- Entire Creator Feed
|
||||||
|
|
||||||
|
- **Flexible Options:**
|
||||||
|
- Specify Page Range (disabled in Manga Mode)
|
||||||
|
- Custom Folder Name for single posts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🧠 Smart Filtering
|
||||||
|
|
||||||
|
- **Character Name Filtering:**
|
||||||
|
- Use `Tifa, Aerith` or group `(Boa, Hancock)` → folder `Boa Hancock`
|
||||||
|
|
||||||
|
- **Filter Scopes:**
|
||||||
|
- `Files`
|
||||||
|
- `Title`
|
||||||
|
- `Both (Title then Files)`
|
||||||
|
- `Comments (Beta - Files first)`
|
||||||
|
|
||||||
|
- **Skip with Words:**
|
||||||
|
- Exclude with `WIP, sketch, preview`
|
||||||
|
|
||||||
|
- **Skip Scopes:**
|
||||||
|
- `Files`
|
||||||
|
- `Posts`
|
||||||
|
- `Both (Posts then Files)`
|
||||||
|
|
||||||
|
- **File Type Filters:**
|
||||||
|
- `All`, `Images/GIFs`, `Videos`, `📦 Only Archives`, `🔗 Only Links`
|
||||||
|
|
||||||
|
- **Filename Cleanup:**
|
||||||
|
- Remove illegal and unwanted characters or phrases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📚 Manga/Comic Mode (Creator Feeds Only)
|
||||||
|
|
||||||
|
- **Chronological Processing** — Oldest posts first
|
||||||
|
|
||||||
|
- **Filename Style Options:**
|
||||||
|
- `Name: Post Title (Default)`
|
||||||
|
- `Name: Original File`
|
||||||
|
- `Name: Date Based (New)`
|
||||||
|
|
||||||
|
- **Best With:** Character filters set to manga/series title
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📁 Folder Structure & Naming
|
||||||
|
|
||||||
|
- **Subfolders:**
|
||||||
|
- Auto-created based on character name, post title, or `Known.txt`
|
||||||
|
|
||||||
|
- "Subfolder per Post" option for further nesting
|
||||||
|
|
||||||
|
- **Smart Naming:** Cleans invalid characters and structures logically
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🖼️ Thumbnail & Compression Tools
|
||||||
|
|
||||||
|
- **Download Thumbnails Only**
|
||||||
|
|
||||||
|
- **Compress to WebP** (via Pillow)
|
||||||
|
- Converts large images to smaller WebP versions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⚙️ Performance Features
|
||||||
|
|
||||||
|
- **Multithreading:**
|
||||||
|
- For both post processing and file downloading
|
||||||
|
|
||||||
|
- **Multi-part Downloads:**
|
||||||
|
- Toggleable in GUI
|
||||||
|
- Splits large files into chunks
|
||||||
|
- Granular chunk-level progress display
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📋 Logging & Progress
|
||||||
|
|
||||||
|
- **Real-time Logs:** Activity, errors, skipped posts
|
||||||
|
|
||||||
|
- **Missed Character Log:** Shows skipped keywords in easy-to-read list
|
||||||
|
|
||||||
|
- **External Links Log:** Shows links (unless disabled in some modes)
|
||||||
|
|
||||||
|
- **Export Links:** Save `.txt` of links (Only Links mode)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🗃️ Config System
|
||||||
|
|
||||||
|
- **Known.txt:**
|
||||||
|
- Stores names for smart folder suggestions
|
||||||
|
- Supports aliases via `(alias1, alias2)`
|
||||||
|
|
||||||
|
- **Stored in Standard App Data Path**
|
||||||
|
|
||||||
|
- **Editable Within GUI**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Installation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Python 3.6 or higher
|
||||||
|
- pip
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install PyQt5 requests Pillow
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
## **🛠️ Build a Standalone Executable (Optional)**
|
||||||
|
|
||||||
|
1. Install PyInstaller:
|
||||||
|
```bash
|
||||||
|
pip install pyinstaller
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run:
|
||||||
|
```bash
|
||||||
|
pyinstaller --name "Kemono Downloader" --onefile --windowed --icon="Kemono.ico" main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Output will be in the `dist/` folder.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
## **🗂 Config Files**
|
||||||
|
|
||||||
|
- `Known.txt` — character/show names used for folder organization
|
||||||
|
- Supports grouped names in format: `(Name1, Name2)`
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
## **💬 Feedback & Support**
|
||||||
|
|
||||||
|
Issues? Suggestions?
|
||||||
|
Open an issue on the [GitHub repository](https://github.com/Yuvi9587/kemono-downloader) or join our community.
|
||||||
|
|||||||
Reference in New Issue
Block a user