134 Commits

Author SHA1 Message Date
Yuvi9587
d5d6fd91ef Commit 2025-06-09 08:35:42 +01:00
Yuvi9587
01665c366b Update main.py 2025-06-09 08:08:40 +01:00
Yuvi9587
b443ec1da9 Update downloader_utils.py 2025-06-09 07:22:07 +01:00
Yuvi9587
ae4ee57500 Update security.md 2025-06-07 23:34:26 +05:30
Yuvi9587
11d0515f8b Merge branch 'main' of https://github.com/Yuvi9587/Kemono-Downloader 2025-06-07 15:58:11 +01:00
Yuvi9587
1b95f13b37 Update readme.md 2025-06-07 16:38:54 +01:00
Yuvi9587
1cb70e2d4d Update 2025-06-07 16:38:23 +01:00
Yuvi9587
2bda267c3e Update readme.md 2025-06-07 20:31:20 +05:30
Yuvi9587
a721900179 Its a Big file 2025-06-07 15:57:24 +01:00
Yuvi9587
b4bea4d4a3 Update security.md 2025-06-07 20:09:50 +05:30
Yuvi9587
373c0c868c Update security.md 2025-06-07 20:09:32 +05:30
Yuvi9587
6960cbed9a Commit 2025-06-07 20:08:11 +05:30
Yuvi9587
8645c0c290 Update drive.py 2025-06-07 19:57:39 +05:30
Yuvi9587
76486a92fd Update security.md 2025-06-07 18:04:01 +05:30
Yuvi9587
823bd438bc Update security.md 2025-06-07 18:03:31 +05:30
Yuvi9587
360c0c247a Update security.md 2025-06-07 18:02:33 +05:30
Yuvi9587
474ba0280a Update security.md 2025-06-07 17:50:02 +05:30
Yuvi9587
d7fa6b1bd6 Commit 2025-06-07 17:48:47 +05:30
Yuvi9587
deb543b596 Create security.md 2025-06-07 17:48:12 +05:30
Yuvi9587
e32eb98bb7 Commit 2025-06-07 17:17:28 +05:30
Yuvi9587
461249b8ba Update 2025-06-07 11:55:56 +01:00
Yuvi9587
f8d67b0555 Commit 2025-06-07 11:49:04 +01:00
Yuvi9587
9701abde5f Update drive.py 2025-06-07 02:56:55 +01:00
Yuvi9587
0940bdb8dd Commit 2025-06-06 17:29:17 +01:00
Yuvi9587
b744e83f09 Update main.py 2025-06-06 14:30:09 +01:00
Yuvi9587
811b7b765c Commit 2025-06-06 13:59:09 +01:00
Yuvi9587
3bc3c7b760 Commit 2025-06-06 13:09:06 +01:00
Yuvi9587
d8ed588033 Commit 2025-06-06 12:49:10 +01:00
Yuvi9587
f6b7919043 Commit 2025-06-06 05:14:07 +01:00
Yuvi9587
401ccd9884 Commit 2025-06-06 04:55:51 +01:00
Yuvi9587
3b010b8eeb Commit 2025-06-05 18:23:08 +01:00
Yuvi9587
da29ccfc1f Commit 2025-06-05 18:00:12 +01:00
Yuvi9587
3197be300f Update downloader_utils.py 2025-06-04 18:47:17 +01:00
Yuvi9587
2cf73e6dbd Update downloader_utils.py 2025-06-04 16:09:00 +01:00
Yuvi9587
bd46002684 Update downloader_utils.py 2025-06-04 04:01:01 +01:00
Yuvi9587
5a6474cb8a commit 2025-06-04 03:36:39 +01:00
Yuvi9587
cdf4e9bdfb Commit 2025-06-03 18:07:20 +01:00
Yuvi9587
10b2ec666f Commit 2025-06-03 17:59:53 +01:00
Yuvi9587
08dac4df1e Commit 2025-06-02 13:13:23 +01:00
Yuvi9587
b3c837e88a Commit 2025-06-02 12:40:07 +01:00
Yuvi9587
e395a8411d Commi 2025-06-02 08:08:10 +01:00
Yuvi9587
ec9e595167 Update readme.md 2025-06-02 04:06:07 +01:00
Yuvi9587
5ff87f914a Commit 2025-06-02 04:04:17 +01:00
Yuvi9587
318b9095a7 Commit 2025-06-02 04:03:34 +01:00
Yuvi9587
437df4e73a Commit 2025-06-02 04:00:39 +01:00
Yuvi9587
3eb26bcf0c Commit 2025-06-02 03:57:14 +01:00
Yuvi9587
db7a08f18a Commit 2025-06-02 03:55:54 +01:00
Yuvi9587
dc1314a148 Update main.py 2025-06-01 12:54:41 +01:00
Yuvi9587
21ba95e325 Update readme.md 2025-06-01 12:53:36 +01:00
Yuvi9587
9367970ec0 Update readme.md 2025-06-01 12:53:01 +01:00
Yuvi9587
c34863a397 Commit 2025-06-01 12:52:44 +01:00
Yuvi9587
f93795e370 Commit 2025-06-01 08:22:29 +01:00
Yuvi9587
7d4e785ca1 commit 2025-05-31 17:23:23 +01:00
Yuvi9587
31b1cb2873 Commit 2025-05-31 15:12:00 +01:00
Yuvi9587
5e23e544e8 Commit 2025-05-31 13:12:03 +01:00
Yuvi9587
80feac092d Commit 2025-05-31 03:38:27 +01:00
Yuvi9587
9e73125d69 readme.md 2025-05-31 03:16:13 +01:00
Yuvi9587
b32cbf0dfd Update main.py 2025-05-30 17:58:25 +01:00
Yuvi9587
a9c9fde855 Commit 2025-05-30 17:57:00 +01:00
Yuvi9587
46658a7bab Update readme.md 2025-05-30 21:09:15 +05:30
Yuvi9587
927c11f2bb Update readme.md 2025-05-30 21:08:23 +05:30
Yuvi9587
a54f2b3567 Commit 2025-05-30 21:04:02 +05:30
Yuvi9587
7f2312b64f Commit 2025-05-30 08:28:11 +05:30
Yuvi9587
7106694bcb Update main.py 2025-05-29 20:29:19 +05:30
Yuvi9587
6b37d73e5a Commit 2025-05-29 19:47:04 +05:30
Yuvi9587
d1c5b205ef Commit 2025-05-29 19:09:08 +05:30
Yuvi9587
10b567a5fd Commit 2025-05-29 19:07:28 +05:30
Yuvi9587
eed0a919aa Commit 2025-05-29 17:56:16 +05:30
Yuvi9587
78357df07f Commit 2025-05-29 08:50:01 +01:00
Yuvi9587
8137c76eb4 Commit 2025-05-28 20:33:06 +05:30
Yuvi9587
be3a522305 Commit 2025-05-28 18:20:54 +05:30
Yuvi9587
13d05765b2 Commit 2025-05-28 13:41:39 +05:30
Yuvi9587
f52d16d1e4 Commit 2025-05-28 04:41:09 +05:30
Yuvi9587
acb91c7e8a Commit 2025-05-28 09:46:03 +05:30
Yuvi9587
c765a7a281 Commit 2025-05-28 09:21:34 +05:30
Yuvi9587
5abfcc8550 Merge branch 'main' of https://github.com/Yuvi9587/Kemono-Downloader 2025-05-28 08:26:03 +05:30
Yuvi9587
7957468077 Commit 2025-05-27 23:00:16 +05:30
Yuvi9587
f774773b63 Commit 2025-05-27 20:34:38 +05:30
Yuvi9587
8036cb9835 Commit 2025-05-26 20:37:37 +05:30
Yuvi9587
13fc33d2c0 Commit 2025-05-26 09:33:45 +05:30
Yuvi9587
8663ef54a3 Commit 2025-05-26 08:43:13 +05:30
Yuvi9587
0316813792 Delete dist directory 2025-05-26 13:55:54 +05:30
Yuvi9587
d201a5396c Delete build/Kemono Downloader directory 2025-05-26 13:55:25 +05:30
Yuvi9587
86f9396b6c Commit 2025-05-26 13:52:34 +05:30
Yuvi9587
0fb4bb3cb0 Commit 2025-05-26 13:52:07 +05:30
Yuvi9587
1528d7ce25 Update Read.png 2025-05-26 09:54:26 +05:30
Yuvi9587
4e7eeb7989 Commit 2025-05-26 09:52:06 +05:30
Yuvi9587
7f2976a4f4 Commit 2025-05-26 09:48:00 +05:30
Yuvi9587
8928cb92da readme.md 2025-05-26 01:39:39 +05:30
Yuvi9587
a181b76124 Update main.py 2025-05-25 17:18:11 +05:30
Yuvi9587
8f085a8f63 Commit 2025-05-25 21:52:04 +05:30
Yuvi9587
93a997351b Update readme.md 2025-05-25 21:22:47 +05:30
Yuvi9587
b3af6c1c15 Commit 2025-05-25 21:21:00 +05:30
Yuvi9587
4a65263f7d Commit 2025-05-25 19:49:17 +05:30
Yuvi9587
1091b5b9b4 Commit 2025-05-25 19:48:08 +05:30
Yuvi9587
f6b3ff2f5c Update main.py 2025-05-25 11:36:35 +05:30
Yuvi9587
b399bdf5cf readme.md 2025-05-25 16:54:35 +05:30
Yuvi9587
9ace161bc8 Update downloader_utils.py 2025-05-25 11:22:04 +05:30
Yuvi9587
66e52cfd78 Commit 2025-05-25 12:27:15 +05:30
Yuvi9587
e665fd3cde Commit 2025-05-25 11:38:38 +05:30
Yuvi9587
fc94f4c691 Commit 2025-05-24 22:55:23 +05:30
Yuvi9587
78e2012f04 Commit 2025-05-24 13:30:06 +05:30
Yuvi9587
3fe9dbacc6 Commit 2025-05-24 13:15:08 +05:30
Yuvi9587
004dea06e0 Commit 2025-05-24 16:22:47 +05:30
Yuvi9587
8994a69c34 Add files via upload 2025-05-24 10:36:15 +05:30
Yuvi9587
f4a692673e main.py 2025-05-24 10:35:46 +05:30
Yuvi9587
4cb5f14ef6 Delete Known.txt 2025-05-23 21:01:05 +05:30
Yuvi9587
a596c4f350 Update main.py 2025-05-23 20:59:35 +05:30
Yuvi9587
e091c60d29 Commit 2025-05-23 20:23:36 +05:30
Yuvi9587
d2ea026a41 Commit 2025-05-23 19:11:52 +05:30
Yuvi9587
bb3d5c20f5 Commit 2025-05-23 18:24:42 +05:30
Yuvi9587
a13eae8f16 Commit 2025-05-23 18:19:30 +05:30
Yuvi9587
7e5dc71720 Commit 2025-05-23 18:06:47 +05:30
Yuvi9587
d7960bbb85 Commit 2025-05-23 17:22:54 +05:30
Yuvi9587
c4d5ba3040 Commit 2025-05-22 07:40:10 +05:30
Yuvi9587
fd84de7bce Commit 2025-05-22 07:03:05 +05:30
Yuvi9587
a6383b20a4 Commit 2025-05-21 17:20:16 +05:30
Yuvi9587
651f9d9f8d Update main.py 2025-05-18 16:17:40 +05:30
Yuvi9587
decef6730f Commit 2025-05-18 16:12:19 +05:30
Yuvi9587
32a12e8a09 Commit 2025-05-17 11:41:43 +05:30
Yuvi9587
62007d2d45 Update readme.md 2025-05-16 16:08:48 +05:30
Yuvi9587
f1e592cf99 Update readme.md 2025-05-16 12:50:32 +05:30
Yuvi9587
bf111d109a Update main.py 2025-05-16 11:37:43 +05:30
Yuvi9587
00f8ff63d6 Commit 2025-05-16 11:23:37 +05:30
Yuvi9587
aee0ff999d Commit 2025-05-15 08:45:32 +05:30
Yuvi9587
b5e9080285 Commit 2025-05-14 16:26:18 +05:30
Yuvi9587
25d33f1531 readme.md 2025-05-13 21:38:55 +05:30
Yuvi9587
ff0ccb2631 Commit 2025-05-13 07:31:09 +05:30
Yuvi9587
da507b2b3a Commit 2025-05-12 18:37:11 +05:30
Yuvi9587
9165903e96 Update main.py 2025-05-12 10:54:57 +05:30
Yuvi9587
f85de58fcb Commit 2025-05-12 10:54:31 +05:30
Yuvi9587
ccfb8496a2 Commit 2025-05-11 15:55:21 +05:30
Yuvi9587
e0d3e1b5af commit 2025-05-10 23:59:00 +05:30
Yuvi9587
50ee50cd5c readme.md 2025-05-10 12:16:45 +05:30
21 changed files with 2302051 additions and 3873 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: [Yuvi9587]

BIN
Kemono.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

197
Known.txt
View File

@@ -1,197 +0,0 @@
Ada
Aeris
Alina
Amara
Anya
Aria
Artemis
Ashe
Astrid
Asuka
Athena
Azura
Belladonna
Bianca
C.C.
Calla
Camilla
Cassia
Celeste
Chika
Clara
Delilah
Dia
Diana
Eira
Elara
Eli
Elise
Elma
Ember
Erza
Esme
Evelyn
Evie
Fiora
Freya
Gasai
Greta
Hanayo
Hancock
Haruhi
Hatsume
Hawkeye
Hinata
Holo
Homura
Ichigo
Illya
Inara
Ino
Isla
Isolde
Ivy
Jeanne
Jinx
Jiro
Juniper
Juvia
Kaelin
Kagome
Kagura
Kaida
Kairi
Kali
Kana
Kanao
Kanna
Kiera
Kikyo
Kirari
Korra
Kotori
Kurisu
Kushina
Kyoko
Lan Fan
Leona
Levy
Lilith
Liora
Lira
Lisanna
Lucia
Lucoa
Lucy
Luna
Lust
Lyra
Madoka
Maia
Makima
Makise
Makomo
Mami
Mari
Marin
Mary
Mavis
Mayuri
Medusa
Mei
Merlin
Mikasa
Milly
Mina
Mion
Mira
Mirabel
Misato
Mitsuri
Momo
Morgana
Nadia
Nami
Naomi
Nelliel
Nerissa
Neve
Nezuko
Noelle
Nova
Nozomi
Nunnally
Nyx
Ochaco
Odette
Ophelia
Orihime
Orla
Perona
Phoebe
Raven
Rei
Reyna
Rhea
Rika
Rin
Rin Tohsaka
Rinoa
Ritsuko
Riza
Robin
Rosalie
Rowan
Ruby
Rukia
Rumi
Saber
Sable
Sakura
Sakura Matou
Sango
Sansa
Satoko
Sayaka
Scáthach
Selene
Seline
Serena
Shinobu
Shion
Shirley
Sierra
Skye
Sophie
Soraya
Sylvia
Talia
Tamayo
Tamsin
Tashigi
Tatiana
Temari
Thalia
Tifa
Toga
Tohru
Tsunade
Umi
Valeria
Viola
Violet
Vivi
Wendy
Winry
Wynne
Yara
Yazawa
Yoruichi
Yoshiko
Yuki Nagato
Yumeko
Yuna
Yuno
Zara
Zelda
Zero Two

24
LICENSE
View File

@@ -1,21 +1,11 @@
MIT License
Custom License - No Commercial Use
Copyright (c) 2025 [Yuvi9587]
Copyright [Yuvi9587] [2025]
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:
Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute the Software for **non-commercial purposes only**, 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.
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
2. Proper credit must be given to the original author in any public use, distribution, or derivative works.
3. Commercial use, resale, or sublicensing of the Software or any derivative works is strictly prohibited without explicit written permission.
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.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND...

BIN
Read/Cat.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 KiB

BIN
Read/Read.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
Read/Read1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
Read/Read2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
Read/Read3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
assets/discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/instagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

2291826
creators.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

205
drive.py Normal file
View File

@@ -0,0 +1,205 @@
from mega import Mega
import os
import requests
import traceback
from urllib .parse import urlparse ,urlunparse ,parse_qs ,urlencode
try :
import gdown
GDOWN_AVAILABLE =True
except ImportError :
GDOWN_AVAILABLE =False
def download_mega_file (mega_link ,download_path =".",logger_func =print ):
"""
Downloads a file from a public Mega.nz link.
Args:
mega_link (str): The public Mega.nz link to the file.
download_path (str, optional): The directory to save the downloaded file.
Defaults to the current directory.
logger_func (callable, optional): Function to use for logging. Defaults to print.
"""
logger_func ("drive.py: download_mega_file called.")
logger_func (f"drive.py: mega_link='{mega_link}', download_path='{download_path}'")
logger_func ("drive.py: Initializing Mega client (Mega())...")
try:
mega_client = Mega()
except Exception as e_init:
logger_func(f"drive.py: ERROR during Mega() instantiation: {e_init}")
traceback.print_exc()
raise
logger_func ("drive.py: Mega client initialized. Logging in anonymously (m.login())...")
try:
m = mega_client.login()
except Exception as e_login:
logger_func(f"drive.py: ERROR during m.login(): {e_login}")
traceback.print_exc()
raise
logger_func ("drive.py: Logged in anonymously.")
logger_func (f"drive.py: Attempting to download from: {mega_link }")
try :
if not os .path .exists (download_path ):
logger_func (f"drive.py: Download path '{download_path }' does not exist. Creating it...")
os .makedirs (download_path ,exist_ok =True )
logger_func (f"drive.py: Download path ensured: '{download_path }'")
logger_func (f"drive.py: Calling m.download_url for '{mega_link }' to '{download_path }'...")
# The download_url method returns the local file path of the downloaded file.
# It takes dest_path (directory) and dest_filename (optional).
# If dest_filename is None, it uses the name from get_public_url_info().
downloaded_file_path = m.download_url(mega_link, dest_path=download_path, dest_filename=None)
logger_func(f"drive.py: m.download_url returned: {downloaded_file_path}")
if downloaded_file_path and os.path.exists(downloaded_file_path):
logger_func(f"drive.py: File downloaded successfully! Saved as: {downloaded_file_path}")
# Optional: Verify size if possible, but get_public_url_info is another network call
# and might be redundant or problematic if the download itself worked.
elif downloaded_file_path:
logger_func(f"drive.py: m.download_url returned a path '{downloaded_file_path}', but it does not exist on disk. Download may have failed silently or path is incorrect.")
raise Exception(f"Mega download_url returned path '{downloaded_file_path}' which was not found.")
else :
logger_func ("drive.py: Download failed. m.download_url did not return a valid file path.")
raise Exception ("Mega download_url did not return a file path or failed.")
except PermissionError as e:
logger_func(f"drive.py: PermissionError: {e}. Denied to write to '{download_path}'. Please check permissions.")
raise
except FileNotFoundError as e:
logger_func(f"drive.py: FileNotFoundError: {e}. The path '{download_path}' is invalid.")
raise
except requests.exceptions.ConnectionError as e: # More specific for network
logger_func(f"drive.py: requests.exceptions.ConnectionError: {e}. Network problem during Mega operation.")
raise
except requests.exceptions.RequestException as e: # General requests error
logger_func(f"drive.py: requests.exceptions.RequestException: {e} during request to Mega.")
raise
except Exception as e: # Catch-all for other errors from mega.py or os calls
logger_func(f"drive.py: An unexpected error occurred during Mega download: {e}")
traceback.print_exc() # Print full traceback for unexpected errors
raise
def download_gdrive_file (gdrive_link ,download_path =".",logger_func =print ):
"""
Downloads a file from a public Google Drive link.
Args:
gdrive_link (str): The public Google Drive link to the file.
download_path (str, optional): The directory to save the downloaded file.
Defaults to the current directory.
logger_func (callable, optional): Function to use for logging. Defaults to print.
"""
if not GDOWN_AVAILABLE :
logger_func ("❌ Error: gdown library is not installed. Cannot download from Google Drive.")
logger_func ("Please install it: pip install gdown")
raise ImportError ("gdown library not found. Please install it: pip install gdown")
logger_func (f"Attempting to download from Google Drive: {gdrive_link }")
try :
if not os .path .exists (download_path ):
logger_func (f"Download path '{download_path }' does not exist. Creating it...")
os .makedirs (download_path ,exist_ok =True )
logger_func (f"Starting Google Drive download to '{download_path }'...")
output_file_path =gdown .download (gdrive_link ,output =download_path ,quiet =False ,fuzzy =True )
if output_file_path and os .path .exists (os .path .join (download_path ,os .path .basename (output_file_path ))):
logger_func (f"✅ Google Drive file downloaded successfully: {output_file_path }")
elif output_file_path :
full_path_check =os .path .join (download_path ,output_file_path )
if os .path .exists (full_path_check ):
logger_func (f"✅ Google Drive file downloaded successfully: {full_path_check }")
else :
logger_func (f"⚠️ Google Drive download finished, gdown returned '{output_file_path }', but file not found at expected location.")
logger_func (f" Please check '{download_path }' for the downloaded file, it might have a different name than expected by gdown's return.")
files_in_dest =[f for f in os .listdir (download_path )if os .path .isfile (os .path .join (download_path ,f ))]
if len (files_in_dest )==1 :
logger_func (f" Found one file in destination: {os .path .join (download_path ,files_in_dest [0 ])}. Assuming this is it.")
elif len (files_in_dest )>1 and output_file_path in files_in_dest :
logger_func (f" Confirmed file '{output_file_path }' exists in '{download_path }'.")
else :
raise Exception (f"gdown download failed or file not found. Returned: {output_file_path }")
else :
logger_func ("❌ Google Drive download failed. gdown did not return an output path.")
raise Exception ("gdown download failed.")
except PermissionError :
logger_func (f"❌ Error: Permission denied to write to '{download_path }'. Please check permissions.")
raise
except Exception as e :
logger_func (f"❌ An error occurred during Google Drive download: {e }")
traceback .print_exc ()
raise
def _get_filename_from_headers (headers ):
cd =headers .get ('content-disposition')
if not cd :
return None
fname_match =re .findall ('filename="?([^"]+)"?',cd )
if fname_match :
return fname_match [0 ].strip ()
return None
def download_dropbox_file (dropbox_link ,download_path =".",logger_func =print ):
"""
Downloads a file from a public Dropbox link.
Args:
dropbox_link (str): The public Dropbox link to the file.
download_path (str, optional): The directory to save the downloaded file.
Defaults to the current directory.
logger_func (callable, optional): Function to use for logging. Defaults to print.
"""
logger_func (f"Attempting to download from Dropbox: {dropbox_link }")
parsed_url =urlparse (dropbox_link )
query_params =parse_qs (parsed_url .query )
query_params ['dl']=['1']
new_query =urlencode (query_params ,doseq =True )
direct_download_url =urlunparse (parsed_url ._replace (query =new_query ))
logger_func (f" Using direct download URL: {direct_download_url }")
try :
if not os .path .exists (download_path ):
logger_func (f"Download path '{download_path }' does not exist. Creating it...")
os .makedirs (download_path ,exist_ok =True )
with requests .get (direct_download_url ,stream =True ,allow_redirects =True ,timeout =(10 ,300 ))as r :
r .raise_for_status ()
filename =_get_filename_from_headers (r .headers )or os .path .basename (urlparse (dropbox_link ).path )or "dropbox_downloaded_file"
filename =re .sub (r'[<>:"/\\|?*]','_',filename )
full_save_path =os .path .join (download_path ,filename )
logger_func (f"Starting Dropbox download of '{filename }' to '{full_save_path }'...")
with open (full_save_path ,'wb')as f :
for chunk in r .iter_content (chunk_size =8192 ):
f .write (chunk )
logger_func (f"✅ Dropbox file downloaded successfully: {full_save_path }")
except Exception as e :
logger_func (f"❌ An error occurred during Dropbox download: {e }")
traceback .print_exc ()
raise
if __name__ =="__main__":
mega_file_link ="https://mega.nz/file/03oRjBQT#Tcbp5sQVIyPbdmv8sLgbb9Lf9AZvZLdKRSQiuXkNW0k"
if not mega_file_link .startswith ("https://mega.nz/file/"):
print ("Invalid Mega file link format. It should start with 'https://mega.nz/file/'.")
else :
script_dir =os .path .dirname (os .path .abspath (__file__ ))
download_directory =os .path .join (script_dir ,"mega_downloads")
print (f"Files will be downloaded to: {download_directory }")
download_mega_file (mega_file_link ,download_directory ,logger_func =print )

341
features.md Normal file
View File

@@ -0,0 +1,341 @@
# Kemono Downloader - Detailed Feature Guide
This guide provides a comprehensive overview of all user interface elements, input fields, buttons, popups, and functionalities available in the Kemono Downloader.
---
## Main Interface & Workflow
These are the primary controls you'll interact with to initiate and manage downloads.
### 1. Main Inputs
- **🔗 Kemono Creator/Post URL Input Field:**
- **Purpose:** This is where you paste the URL of the content you want to download.
- **Usage:** Supports full URLs for:
- Kemono.su (and mirrors like kemono.party) creator pages (e.g., `https://kemono.su/patreon/user/12345`).
- Kemono.su (and mirrors) individual posts (e.g., `https://kemono.su/patreon/user/12345/post/98765`).
- Coomer.party (and mirrors like coomer.su) creator pages.
- Coomer.party (and mirrors) individual posts.
- **Note:** When **⭐ Favorite Mode** is active, this field is disabled and shows a "Favorite Mode active" message.
- **🎨 Creator Selection Button:**
- **Icon:** 🎨 (Artist Palette)
- **Location:** Next to the URL input field.
- **Purpose:** Opens the "Creator Selection" dialog to easily add multiple creators to the URL field.
- **Dialog Features:**
- Loads creators from your `creators.json` file (expected in the app's directory).
- **Search Bar:** Filter the list of creators by name.
- **Creator List:** Displays creators with their service (e.g., Patreon, Fanbox) and ID.
- **Selection:** Checkboxes to select one or more creators.
- **"Add Selected to URL" Button:** Adds the names of selected creators to the URL input field, comma-separated.
- **"Download Scope" Radio Buttons (`Characters` / `Creators`):** Determines the folder structure for items added via this popup.
- `Characters`: Assumes creator names are character names for folder organization.
- `Creators`: Uses the actual creator names for folder organization.
- **Page Range (Start to End) Input Fields:**
- **Purpose:** For creator URLs, specify a range of pages to fetch and process.
- **Usage:** Enter the starting page number in the first field and the ending page number in the second.
- **Behavior:**
- If left blank, all pages for the creator are typically processed (or up to a reasonable limit).
- Disabled for single post URLs or when **📖 Manga/Comic Mode** is active (as manga mode fetches all posts for chronological sorting).
- **📁 Download Location Input Field & Browse Button:**
- **Purpose:** Specify the main directory where all downloaded files and folders will be saved.
- **Usage:**
- Type or paste the path directly into the field.
- Click the **"Browse..."** button to open a system dialog to select a folder.
- **Requirement:** This field must be filled unless you are using the "🔗 Only Links" filter mode.
### 2. Action Buttons
- **⬇️ Start Download / 🔗 Extract Links Button:**
- **Purpose:** The primary action button to begin the downloading or link extraction process based on current settings.
- **Behavior:**
- If "🔗 Only Links" filter is selected, the button text changes to **"🔗 Extract Links"** and it will only gather external links from posts.
- Otherwise, it reads **"⬇️ Start Download"** and initiates the content download.
- **⏸️ Pause / ▶️ Resume Download Button:**
- **Purpose:** Temporarily halt or continue the ongoing download/extraction process.
- **Behavior:**
- When active, the button shows **"⏸️ Pause Download"**. Clicking it pauses the operation.
- When paused, the button shows **"▶️ Resume Download"**. Clicking it resumes from where it left off.
- Some UI settings can be changed while paused (e.g., filter adjustments), which will apply upon resuming.
- **❌ Cancel & Reset UI Button:**
- **Purpose:** Immediately stops the current download/extraction operation and performs a "soft" reset of the UI.
- **Behavior:**
- Halts all active threads and processes.
- Clears progress information and logs.
- Preserves the content of the "🔗 Kemono Creator/Post URL" and "📁 Download Location" input fields. Other settings are reset to their defaults.
- **🔄 Reset Button (located in the log area):**
- **Purpose:** Performs a "hard" reset of the UI when no operation is active.
- **Behavior:**
- Clears all input fields (including URL and Download Location).
- Resets all filter settings and options to their default values.
- Clears the log area.
---
## Filtering & Content Selection
These options allow you to precisely control what content is downloaded or skipped.
- **🎯 Filter by Character(s) Input Field:**
- **Purpose:** Download content related to specific characters.
- **Usage:** Enter character names, comma-separated.
- **Advanced Syntax:**
- `Nami`: Simple character filter. Matches "Nami".
- `(Vivi, Ulti, Uta)`: Grouped characters. Matches "Vivi" OR "Ulti" OR "Uta". If "Separate Folders" is on, creates a shared folder for the session (e.g., "Vivi Ulti Uta"). Adds "Vivi", "Ulti", "Uta" as *separate* entries to `Known.txt` if new.
- `(Boa, Hancock)~`: Aliased characters. Matches "Boa" OR "Hancock" but treats them as the same entity. If "Separate Folders" is on, creates a shared folder (e.g., "Boa Hancock"). Adds "Boa Hancock" as a *single group entry* to `Known.txt` if new, with "Boa" and "Hancock" as its aliases.
- **Filter: [Type] Button (Scope for Character Filter):**
- **Location:** Next to the "Filter by Character(s)" input.
- **Purpose:** Defines how the character filter is applied. Cycles through options on click.
- **Options:**
- `Filter: Files`: Checks individual filenames against the character filter. Only matching files from a post are downloaded.
- `Filter: Title` (Default): Checks post titles against the character filter. If the title matches, all files from that post are downloaded.
- `Filter: Both`: Checks the post title first. If no match, then checks individual filenames within that post.
- `Filter: Comments (Beta)`: Checks filenames first. If no file match, then checks post comments/description. (Note: This may use more API requests).
- **🚫 Skip with Words Input Field:**
- **Purpose:** Exclude posts or files containing specified keywords.
- **Usage:** Enter words or phrases, comma-separated (e.g., `WIP, sketch, preview`).
- **Scope: [Type] Button (Scope for Skip with Words):**
- **Location:** Next to the "Skip with Words" input.
- **Purpose:** Defines how the skip words are applied. Cycles through options on click.
- **Options:**
- `Scope: Files`: Skips individual files if their names contain any of the skip words.
- `Scope: Posts` (Default): Skips entire posts if their titles contain any of the skip words.
- `Scope: Both`: Checks the post title first. If no skip words match, then checks individual filenames.
- **✂️ Remove Words from name Input Field:**
- **Purpose:** Clean up downloaded filenames by removing specified unwanted words or phrases.
- **Usage:** Enter words or phrases, comma-separated (e.g., `patreon, [HD], kemono`).
- **Filter Files (Radio Buttons):**
- **Purpose:** Select the types of files to download.
- **Options:**
- `All`: Download all file types attached to posts.
- `Images/GIFs`: Download only common image formats (JPG, PNG, GIF, WebP, etc.).
- `Videos`: Download only common video formats (MP4, MOV, MKV, WebM, etc.).
- `📦 Only Archives`: Exclusively download `.zip` and `.rar` files. This mode disables the "Skip .zip/.rar" checkboxes and the "Show External Links in Log" feature.
- `🎧 Only Audio`: Download only common audio formats (MP3, WAV, FLAC, OGG, etc.).
- `🔗 Only Links`: Do not download any files. Instead, extract and display external links found in post descriptions in the log area. The main action button changes to "🔗 Extract Links".
- **Skip .zip / Skip .rar Checkboxes:**
- **Purpose:** Individually choose to skip downloading `.zip` files or `.rar` files.
- **Behavior:** Disabled if the "📦 Only Archives" filter is active.
---
## Download Customization
Options to further refine the download process and output.
- **Download Thumbnails Only Checkbox:**
- **Purpose:** Download only the small preview images (thumbnails) provided by the API, instead of full-resolution files.
- **Behavior:** If "**Scan Content for Images**" is also active, this option's behavior changes: *only* images found by the content scan (embedded `<img>` tags) are downloaded as thumbnails (API thumbnails are ignored).
- **Scan Content for Images Checkbox:**
- **Purpose:** Actively scan the HTML content of posts for `<img>` tags and direct image links. This is crucial for downloading images embedded in post descriptions that are not listed as direct attachments in the API response.
- **Behavior:** Resolves relative image paths to absolute URLs for downloading.
- **Compress to WebP Checkbox:**
- **Purpose:** Convert downloaded images to WebP format to potentially save disk space.
- **Requirement:** Requires the `Pillow` library to be installed.
- **Behavior:** Attempts to convert images larger than a certain threshold (e.g., 1.5MB) to WebP if the WebP version is significantly smaller. Original files are not kept if conversion is successful.
- **🗄️ Custom Folder Name (Single Post Only) Input Field:**
- **Purpose:** When downloading a single post URL, allows you to specify a custom name for the folder where its contents will be saved.
- **Visibility:** Only appears if:
1. A single post URL is entered in the main URL field.
2. The "**Separate Folders by Name/Title**" option is enabled.
---
## 📖 Manga/Comic Mode
Specialized mode for downloading creator feeds in a way suitable for sequential reading, like manga or comics. This mode is implicitly active when downloading from a creator URL and certain filename styles are chosen.
- **Activation:** Primarily by downloading a creator's feed (not a single post) and selecting a relevant "Filename Style".
- **Core Behavior:** Processes and downloads posts from the creator's feed in chronological order (oldest to newest). The "Page Range" input is typically disabled as all posts are fetched for correct sorting.
- **Filename Style Toggle Button (located in the log area):**
- **Purpose:** Controls how files are named when downloading in a manga/comic-like fashion. Cycles through options on click.
- **Options:**
- `Name: Post Title` (Default for non-manga): The first file in a post is named after the post title; subsequent files in the *same post* keep their original names.
- `Name: Original File`: All downloaded files attempt to keep their original filenames as provided by the server. An optional "Filename Prefix" input field appears.
- `Name: Title+G.Num`: (Global Numbering) All files across all downloaded posts for the creator get a prefix from their respective post's title, followed by a global sequential number (e.g., `Chapter 1_001.jpg`, `Chapter 1_002.jpg`, `Chapter 2_003.jpg`). This ensures strict order across posts. Disables post-level multithreading for sequential numbering.
- `Name: Date Based`: Files are named sequentially (e.g., `001.jpg`, `002.jpg`) based on the post's publication date. An optional "Filename Prefix" input field appears. Disables post-level multithreading.
- **Optional Filename Prefix Input Field (Manga Mode):**
- **Visibility:** Appears when "Filename Style" is set to `Name: Original File` or `Name: Date Based`.
- **Purpose:** Allows you to add a custom prefix to all filenames generated using these styles (e.g., `MySeries_001.jpg`).
---
## Folder Organization
Controls for how downloaded content is structured into folders.
- **Separate Folders by Name/Title Checkbox:**
- **Purpose:** Creates subfolders within the main "Download Location" based on matching criteria.
- **Behavior:**
- If "**Filter by Character(s)**" is used, folders are named after the matched character(s)/group(s).
- If no character filter matches (or no filter is active), but the post title matches an entry in `Known.txt`, a folder named after the `Known.txt` entry is created.
- If neither of the above, and this option is checked, folders might be created based on post titles directly (behavior can vary).
- **Subfolder per Post Checkbox:**
- **Purpose:** Creates an additional layer of subfolders, where each individual post's content goes into its own subfolder.
- **Behavior:** Only active if "**Separate Folders by Name/Title**" is also checked. The post subfolder will be created *inside* the character/title folder. Folder names are typically derived from sanitized post titles or IDs.
- **`Known.txt` Management UI (Bottom Left of UI):**
- **Purpose:** Manages a local list (`Known.txt` file in the app directory) of series, characters, or general terms used for automatic folder organization and character filter suggestions.
- **Elements:**
- **List Display:** Shows the primary names from your `Known.txt` file.
- **Add New Input Field:** Enter a new name or group to add to `Known.txt`.
- Simple Name: e.g., `My Series`
- Group (creates separate entries in `Known.txt`): e.g., `(Vivi, Ulti, Uta)`
- Group with Aliases (single entry in `Known.txt` with `~`): e.g., `(Boa, Hancock)~`
- ** Add Button:** Adds the entry from the "Add New" field to `Known.txt` and refreshes the list.
- **⤵️ Add to Filter Button:** Opens a dialog displaying all entries from `Known.txt` (with a search bar). Select one or more entries to add them to the "**🎯 Filter by Character(s)**" input field. Grouped names from `Known.txt` are added with the `~` syntax if applicable.
- **🗑️ Delete Selected Button:** Removes the currently selected name(s) from the list display and from the `Known.txt` file.
- **Open Known.txt Button:** Opens your `Known.txt` file in the system's default text editor for manual editing.
- **❓ Help Button (Known.txt):** Opens a guide or tooltip explaining the `Known.txt` feature and syntax.
---
## ⭐ Favorite Mode (Kemono.su Only)
Download directly from your favorited artists and posts on Kemono.su.
- **Enable Checkbox ("⭐ Favorite Mode"):**
- **Location:** Usually near the "🔗 Only Links" filter option.
- **Purpose:** Switches the downloader to operate on your Kemono.su favorites.
- **UI Changes upon Enabling:**
- The "🔗 Kemono Creator/Post URL" input field is disabled/replaced with a "Favorite Mode active" message.
- The main action buttons change to "**🖼️ Favorite Artists**" and "**📄 Favorite Posts**".
- The "**🍪 Use Cookie**" option is automatically enabled and locked, as cookies are required to access your favorites.
- **🖼️ Favorite Artists Button & Dialog:**
- **Purpose:** Fetches and allows you to download content from artists you have favorited on Kemono.su.
- **Dialog Features:**
- Fetches the list of your favorited artists.
- **Search Bar:** Filter artists by name.
- **Artist List:** Displays favorited artists.
- **Select All / Deselect All:** Convenience buttons for selection.
- **"Download Selected" Button:** Queues all posts from the selected artists for download, respecting current filter settings.
- **📄 Favorite Posts Button & Dialog:**
- **Purpose:** Fetches and allows you to download specific posts you have favorited on Kemono.su.
- **Dialog Features:**
- Fetches the list of your favorited posts, usually grouped by artist and sorted by date.
- **Search Bar:** Filter posts by title, creator name, ID, or service.
- **Post List:** Displays favorited posts. Known names from your `Known.txt` may be highlighted in post titles for easier identification.
- **Select All / Deselect All:** Convenience buttons for selection.
- **"Download Selected" Button:** Queues the selected individual posts for download, respecting current filter settings.
- **Favorite Download Scope Button (Location may vary, often near Favorite Posts button):**
- **Purpose:** Determines the folder structure for downloads initiated via Favorite Mode.
- **Options:**
- `Scope: Selected Location`: All selected favorites (artists or posts) are downloaded directly into the main "📁 Download Location". Global filters apply.
- `Scope: Artist Folders`: A subfolder is created for each artist within the main "📁 Download Location" (e.g., `DownloadLocation/ArtistName/`). Content from that artist (whether a full artist download or specific favorited posts from them) goes into their respective subfolder. Filters apply within each artist's context.
---
## Advanced & Performance
- **🍪 Cookie Management:**
- **Use Cookie Checkbox:** Enables the use of browser cookies for accessing content that might be restricted or require login (e.g., certain posts, Favorite Mode).
- **Cookie Text Field:**
- **Purpose:** Directly paste your cookie string.
- **Format:** Standard HTTP cookie string format (e.g., `name1=value1; name2=value2`).
- **Browse... Button (for Cookies):**
- **Purpose:** Select a `cookies.txt` file from your system.
- **Format:** Must be in Netscape cookie file format.
- **Behavior:**
- The text field takes precedence if filled.
- If "Use Cookie" is checked and both the text field and browsed file path are empty, the application will attempt to automatically load a `cookies.txt` file from its root directory.
- **Use Multithreading Checkbox & Threads Input Field:**
- **Purpose:** Enable and configure the number of simultaneous operations to potentially speed up downloads.
- **Behavior:**
- **Creator Feeds:** The "Threads" input controls how many posts are processed concurrently.
- **Single Post URLs:** The "Threads" input controls how many files from that single post are downloaded concurrently.
- **Note:** Setting too high a number might lead to API rate-limiting or instability.
- **Multi-part Download Toggle Button (located in the log area):**
- **Purpose:** Enables/disables multi-segment downloading for individual large files.
- **Options:**
- `Multi-part: ON`: Large files are split into multiple parts that are downloaded simultaneously and then reassembled. Can significantly speed up downloads for single large files but may increase UI choppiness or log spam with many small files.
- `Multi-part: OFF` (Default): Files are downloaded as a single stream.
- **Behavior:** Disabled if "🔗 Only Links" or "📦 Only Archives" mode is active.
---
## Logging & Monitoring
- **📜 Progress Log / Extracted Links Log Area:**
- **Purpose:** The main text area displaying detailed messages about the ongoing process.
- **Content:** Shows download progress for each file, errors encountered, skipped items, summary information, or extracted links (if in "🔗 Only Links" mode).
- **👁️ / 🙈 Log View Toggle Button:**
- **Purpose:** Switches the content displayed in the main log area.
- **Views:**
- `👁️ Progress Log` (Default): Shows all download activity, errors, and general progress messages.
- `🙈 Missed Character Log`: Shows a list of key terms intelligently extracted from post titles or content that were skipped due to the "**🎯 Filter by Character(s)**" not matching. Useful for identifying characters you might want to add to your filter or `Known.txt`.
- **Show External Links in Log Checkbox & Panel:**
- **Purpose:** If checked, a secondary, smaller log panel appears (usually below the main log) that specifically displays any external links (e.g., to Mega, Google Drive) found in post descriptions.
- **Behavior:** Disabled if "🔗 Only Links" or "📦 Only Archives" mode is active (as "Only Links" uses the main log, and archives typically don't have such external links processed).
- **Export Links Button:**
- **Visibility:** Appears when the "**🔗 Only Links**" filter mode is active.
- **Purpose:** Saves all the links extracted and displayed in the main log area to a `.txt` file.
- **Progress Labels/Bars:**
- **Purpose:** Provide a visual and textual representation of the download progress.
- **Typically Includes:**
- Overall post progress (e.g., "Post 5 of 20").
- Individual file download status (e.g., "Downloading file.zip... 50% at 1.2 MB/s").
- Summary statistics at the end of a session (total downloaded, skipped, failed).
---
## Error Handling & Retries
- **🆘 Error Button (Main UI):**
- **Location:** Typically near the main action buttons (e.g., Start, Pause, Cancel).
- **Purpose:** Becomes active if files failed to download during the last session (and were not successfully retried). Clicking it opens the "Files Skipped Due to Errors" dialog.
- **"Files Skipped Due to Errors" Dialog:**
- **File List:** Displays a list of files that encountered download errors. Each entry shows the filename, the post it was from (title and ID).
- **Checkboxes:** Allows selection of individual files from the list.
- **"Select All" Button:** Checks all files in the list.
- **"Retry Selected" Button:** Attempts to re-download all checked files.
- **"Export URLs to .txt" Button:**
- Opens an "Export Options" dialog.
- **"Link per line (URL only)":** Exports only the direct download URL for each failed file, one URL per line.
- **"Export with details (URL [Post, File info])":** Exports the URL followed by details like Post Title, Post ID, and Original Filename in brackets.
- Prompts the user to save the generated `.txt` file.
- **"OK" Button:** Closes the dialog.
- **Note:** Files successfully retried or skipped due to hash match during a retry attempt are removed from this error list.
---
## Other UI Elements
- **Retry Failed Downloads Prompt:**
- **Trigger:** Appears at the end of a download session if there were files that failed to download due to recoverable errors (e.g., network interruption, IncompleteRead).
- **Action:** Prompts the user if they want to attempt downloading the failed files again.
- **New Name Confirmation Dialog (for Character Filter & `Known.txt`):**
- **Trigger:** When new, unrecognized names or groups are used in the "**🎯 Filter by Character(s)**" field that are not present in `Known.txt`.
- **Action:** Prompts the user to confirm if they want to add these new names/groups to `Known.txt` with the appropriate formatting (simple, grouped, or aliased).
- **Onboarding Tour / Help Guide Button (❓):**
- **Purpose:** Opens a built-in help guide or an onboarding tour that explains the basic functionalities and UI elements of the application. Often linked to this detailed feature guide.
---
This guide should cover all interactive elements of the Kemono Downloader. If you have further questions or discover elements not covered, please refer to the main `readme.md` or consider opening an issue on the project's repository.

9096
main.py

File diff suppressed because it is too large Load Diff

237
multipart_downloader.py Normal file
View File

@@ -0,0 +1,237 @@
import os
import time
import requests
import hashlib
import http.client
import traceback
import threading
import queue
from concurrent.futures import ThreadPoolExecutor, as_completed
CHUNK_DOWNLOAD_RETRY_DELAY = 2
MAX_CHUNK_DOWNLOAD_RETRIES = 1
DOWNLOAD_CHUNK_SIZE_ITER = 1024 * 256
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,
logger_func, emitter=None, api_original_filename=None):
if cancellation_event and cancellation_event.is_set():
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Download cancelled before start.")
return 0, False
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
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)
logger_func(f" [Chunk {part_num + 1}/{total_parts}] Download resumed.")
chunk_headers = headers.copy()
if end_byte != -1 :
chunk_headers['Range'] = f"bytes={start_byte}-{end_byte}"
elif start_byte == 0 and end_byte == -1:
pass
bytes_this_chunk = 0
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
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:
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)))
last_speed_calc_time = time.time()
bytes_at_last_speed_calc = bytes_this_chunk
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)
response = requests.get(chunk_url, headers=chunk_headers, timeout=(10, 120), stream=True, cookies=cookies_for_chunk)
response.raise_for_status()
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:
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 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:
f.write(data_segment)
bytes_this_chunk += len(data_segment)
with progress_data['lock']:
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:
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
if emitter and (current_time - global_emit_time_ref[0] > 0.25):
global_emit_time_ref[0] = current_time
status_list_copy = [dict(s) for s in progress_data['chunks_status']]
if isinstance(emitter, queue.Queue):
emitter.put({'type': 'file_progress', 'payload': (api_original_filename, status_list_copy)})
elif hasattr(emitter, 'file_progress_signal'):
emitter.file_progress_signal.emit(api_original_filename, status_list_copy)
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 isinstance(e, requests.exceptions.ConnectionError) and \
("Failed to resolve" in str(e) or "NameResolutionError" in str(e)):
logger_func(" 💡 This looks like a DNS resolution problem. Please check your internet connection, DNS settings, or VPN.")
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:
logger_func(f" ❌ [Chunk {part_num + 1}/{total_parts}] Non-retryable error: {e}")
if ("Failed to resolve" in str(e) or "NameResolutionError" in str(e)):
logger_func(" 💡 This looks like a DNS resolution problem. Please check your internet connection, DNS settings, or VPN.")
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
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
def download_file_in_parts(file_url, save_path, total_size, num_parts, headers, api_original_filename,
emitter_for_multipart, cookies_for_chunk_session,
cancellation_event, skip_event, logger_func, pause_event):
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)
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:
chunks_ranges.append((start, end))
elif total_size == 0 and i == 0:
chunks_ranges.append((0, -1))
chunk_actual_sizes = []
for start, end in chunks_ranges:
if end == -1 and start == 0:
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,
'total_downloaded_so_far': 0,
'chunks_status': [
{'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(),
'last_global_emit_time': [time.time()]
}
chunk_futures = []
all_chunks_successful = True
total_bytes_from_chunks = 0
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, global_emit_time_ref=progress_data['last_global_emit_time'],
pause_event=pause_event, cookies_for_chunk=cookies_for_chunk_session, logger_func=logger_func, emitter=emitter_for_multipart,
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
if emitter_for_multipart:
with progress_data['lock']:
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'):
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''):
md5_hasher.update(buf)
calculated_hash = md5_hasher.hexdigest()
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

227
readme.md
View File

@@ -1,105 +1,154 @@
# Kemono Downloader v3.1.0
<h1 align="center">Kemono Downloader v5.1.0</h1>
A feature-rich GUI application built with PyQt5 to download content from [Kemono.su](https://kemono.su) or [Coomer.party](https://coomer.party). Offers robust filtering, smart organization, manga-specific handling, and performance tuning. Now with session resuming, better retry logic, and smarter file management.
<table align="center">
<tr>
<td align="center">
<img src="Read/Read.png" alt="Post Downloader Tab" width="400"/>
<br>
<strong>Default</strong>
</td>
<td align="center">
<img src="Read/Read1.png" alt="Creator Downloader Tab" width="400"/>
<br>
<strong>Favorite mode</strong>
</td>
</tr>
<tr>
<td align="center">
<img src="Read/Read2.png" alt="Settings Tab" width="400"/>
<br>
<strong>Single Post</strong>
</td>
<td align="center">
<img src="Read/Read3.png" alt="Settings Tab" width="400"/>
<br>
<strong>Manga/Comic Mode</strong>
</td>
<td align="center">
</td>
</tr>
</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).
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.1.0 enhances error handling and UI responsiveness.*
<p align="center">
<a href="features.md"><strong>📚 Full Feature List</strong></a> •
<a href="LICENSE"><strong>📝 License</strong></a>
</p>
---
## 🚀 What's New in v3.1.0
## Feature Overview
* **Session Resuming**
* Automatically saves and resumes incomplete downloads.
Kemono Downloader offers a range of features to streamline your content downloading experience:
* **Retry on Failure**
* Failed files auto-retry up to 3 times.
* Clear logging for each retry attempt.
* **Batch URL Import**
* Load multiple creator or post URLs from a `.txt` file.
* **Improved Manga Mode**
* Better post ordering and handling of missing or untitled posts.
* Optional numeric-only sorting for consistent naming.
* **UI Enhancements**
* Settings persist across sessions.
* Improved layout spacing, tooltips, and status indicators.
* **Stability & Speed**
* Faster post fetching with lower memory usage.
* Minor bug fixes (duplicate folders, empty post crashes).
- **User-Friendly Interface:** A modern PyQt5 GUI for easy navigation and operation.
- **Flexible Downloading:**
- Download content from Kemono.su (and mirrors) and Coomer.party (and mirrors).
- Supports creator pages (with page range selection) and individual post URLs.
- Standard download controls: Start, Pause, Resume, and Cancel.
- **Powerful Filtering:**
- **Character Filtering:** Filter content by character names. Supports simple comma-separated names and grouped names for shared folders.
- **Keyword Skipping:** Skip posts or files based on specified keywords.
- **Filename Cleaning:** Remove unwanted words or phrases from downloaded filenames.
- **File Type Selection:** Choose to download all files, or limit to images/GIFs, videos, audio, or archives. Can also extract external links only.
- **Customizable Downloads:**
- **Thumbnails Only:** Option to download only small preview images.
- **Content Scanning:** Scan post HTML for `<img>` tags and direct image links, useful for images embedded in descriptions.
- **WebP Conversion:** Convert images to WebP format for smaller file sizes (requires Pillow library).
- **Organized Output:**
- **Automatic Subfolders:** Create subfolders based on character names (from filters or `Known.txt`) or post titles.
- **Per-Post Subfolders:** Option to create an additional subfolder for each individual post.
- **Manga/Comic Mode:**
- Downloads posts from a creator's feed in chronological order (oldest to newest).
- Offers various filename styling options for sequential reading (e.g., post title, original name, global numbering).
- **⭐ Favorite Mode:**
- Directly download from your favorited artists and posts on Kemono.su.
- Requires a valid cookie and adapts the UI for easy selection from your favorites.
- Supports downloading into a single location or artist-specific subfolders.
- **Performance & Advanced Options:**
- **Cookie Support:** Use cookies (paste string or load from `cookies.txt`) to access restricted content.
- **Multithreading:** Configure the number of simultaneous downloads/post processing threads for improved speed.
- **Logging:**
- A detailed progress log displays download activity, errors, and summaries.
---
## 🧩 Core Features
* **Simple GUI**
Built with PyQt5 for a clean, responsive experience.
* **Supports Both Post and Creator URLs**
Download a single post or an entire feed with one click.
* **Smart Folder System**
Organize files using post titles, known character/show names, or a folder per post.
Detects and auto-names folders based on custom keywords.
* **Known Names Manager**
Add, search, and delete tags for smarter organization.
Saved to `Known.txt` for reuse.
* **Advanced Filters**
* Skip posts or files with specific keywords (e.g. `WIP`, `sketch`).
* Filter by media type: images, videos, or GIFs.
* Skip `.zip` and `.rar` archives.
* **Manga Mode**
Rename and sort manga posts by title and upload order.
Handles one-image-per-post formats cleanly.
* **Image Compression**
Auto-convert large images (>1.5MB) to WebP (requires Pillow).
* **Multithreaded Downloads**
Adjustable worker count with warnings at unsafe levels.
Full threading for creators, single-thread fallback for post mode.
* **Download Controls**
Cancel, pause, or skip files mid-download.
Visual progress tracking with per-post summaries.
* **Dark Mode**
Clean and modern dark-themed interface.
## ✨ What's New in v5.1.0
- **Enhanced Error File Management**: The "Error" button now opens a dialog listing files that failed to download. This dialog includes:
- An option to **retry selected** failed downloads.
- A new **"Export URLs to .txt"** button, allowing users to save links of failed downloads either as "URL only" or "URL with details" (including post title, ID, and original filename).
- Fixed a bug where files skipped during retry (due to existing hash match) were not correctly removed from the error list.
- **Improved UI Stability**: Addressed issues with UI state management to more accurately reflect ongoing download activities (including retries and external link downloads). This prevents the "Cancel" button from becoming inactive prematurely while operations are still running.
---
## 🔧 Backend Enhancements
* **Session File Support**
Downloads can be resumed even after a crash or restart.
Session progress is saved automatically.
* **Retry Logic**
Auto-retries individual failed files before skipping.
Logs all failures with HTTP codes and reasons.
* **Hash-Based Deduplication**
Prevents redownloading of previously saved files.
* **Smart Naming**
Cleans and standardizes inconsistent post titles.
Adds page indices for manga.
* **Efficient Logging**
Toggle between basic and advanced views.
Live feedback with color-coded logs.
---
## 📦 Installation
## Installation
### Requirements
- Python 3.6 or higher
- pip (Python package installer)
* Python 3.6+
* Pip packages:
### Install Dependencies
Open your terminal or command prompt and run:
```bash
pip install PyQt5 requests Pillow
pip install PyQt5 requests Pillow mega.py
```
### Running the Application
Navigate to the application's directory in your terminal and run:
```bash
python main.py
```
### Optional Setup
- **Main Inputs:**
- Place your `cookies.txt` in the root directory (if using cookies).
- Prepare your `Known.txt` and `creators.json` in the same directory for advanced filtering and selection features.
---
## Troubleshooting
### AttributeError: module 'asyncio' has no attribute 'coroutine'
If you encounter an error message similar to:
```
AttributeError: module 'asyncio' has no attribute 'coroutine'. Did you mean: 'coroutines'?
```
This usually means that a dependency, often `tenacity` (used by `mega.py`), is an older version that's incompatible with your Python version (typically Python 3.10+).
To fix this, activate your virtual environment and run the following commands to upgrade the libraries:
```bash
pip install --upgrade tenacity
pip install --upgrade mega.py
```
---
## Contribution
Feel free to fork this repo and submit pull requests for bug fixes, new features, or UI improvements!
---
## License
This project is under the Custom Licence
## Star History
<table align="center" style="border-collapse: collapse; border: none; margin-left: auto; margin-right: auto;">
<tr>
<td align="center" valign="middle" style="padding: 10px; border: none;">
<a href="https://www.star-history.com/#Yuvi9587/Kemono-Downloader&Date">
<img src="https://api.star-history.com/svg?repos=Yuvi9587/Kemono-Downloader&type=Date" alt="Star History Chart" width="650">
</a>
</table>
👉 See [features.md](features.md) for the full feature list.

19
security.md Normal file
View File

@@ -0,0 +1,19 @@
# Security Policy
## Supported Versions
We are committed to maintaining and improving the Kemono Downloader. For the best experience and access to the latest security updates and features, we strongly recommend using the most recent versions of the application.
| Version | Supported Status |
| -------------- | ------------------------------------ |
| >= 5.0.0 | :white_check_mark: Actively Supported |
| 4.0.0 - 4.x.x | :warning: Supported (Limited Features) |
| < 4.0.0 | :x: End of Life (EOL) |
Users are encouraged to update to **v5.0.0 or newer** versions.
## Active Maintenance
The Kemono Downloader is actively maintained. We strive to address bugs, implement new features in a timely manner. If you discover any security vulnerabilities, please report them(details on reporting to be added if a formal process is established).
We appreciate your help in keeping Kemono Downloader secure!

325
tour.py
View File

@@ -1,325 +0,0 @@
import sys
import traceback # Added for enhanced error reporting
from PyQt5.QtWidgets import (
QApplication, QDialog, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout,
QStackedWidget, QSpacerItem, QSizePolicy, QCheckBox, QDesktopWidget
)
from PyQt5.QtCore import Qt, QSettings, pyqtSignal
class TourStepWidget(QWidget):
"""A single step/page in the tour."""
def __init__(self, title_text, content_text, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(10) # Adjusted spacing between title and content for bullet points
title_label = QLabel(title_text)
title_label.setAlignment(Qt.AlignCenter)
# Increased padding-bottom for more space below title
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;")
content_label = QLabel(content_text)
content_label.setWordWrap(True)
content_label.setAlignment(Qt.AlignLeft)
content_label.setTextFormat(Qt.RichText)
# Adjusted line-height for bullet point readability
content_label.setStyleSheet("font-size: 11pt; color: #C8C8C8; line-height: 1.8;")
layout.addWidget(title_label)
layout.addWidget(content_label)
layout.addStretch(1)
class TourDialog(QDialog):
"""
A dialog that shows a multi-page tour to the user.
Includes a "Never show again" checkbox.
Uses QSettings to remember this preference.
"""
tour_finished_normally = pyqtSignal()
tour_skipped = pyqtSignal()
CONFIG_ORGANIZATION_NAME = "KemonoDownloader"
CONFIG_APP_NAME_TOUR = "ApplicationTour"
TOUR_SHOWN_KEY = "neverShowTourAgainV3" # Updated key for new tour content
def __init__(self, parent=None):
super().__init__(parent)
self.settings = QSettings(self.CONFIG_ORGANIZATION_NAME, self.CONFIG_APP_NAME_TOUR)
self.current_step = 0
self.setWindowTitle("Welcome to Kemono Downloader!")
self.setModal(True)
# Set fixed square size, smaller than main window
self.setFixedSize(600, 620) # Slightly adjusted for potentially more text
self.setStyleSheet("""
QDialog {
background-color: #2E2E2E;
border: 1px solid #5A5A5A;
}
QLabel {
color: #E0E0E0;
}
QCheckBox {
color: #C0C0C0;
font-size: 10pt;
spacing: 5px;
}
QCheckBox::indicator {
width: 13px;
height: 13px;
}
QPushButton {
background-color: #555;
color: #F0F0F0;
border: 1px solid #6A6A6A;
padding: 8px 15px;
border-radius: 4px;
min-height: 25px;
font-size: 11pt;
}
QPushButton:hover {
background-color: #656565;
}
QPushButton:pressed {
background-color: #4A4A4A;
}
""")
self._init_ui()
self._center_on_screen()
def _center_on_screen(self):
"""Centers the dialog on the screen."""
try:
screen_geometry = QDesktopWidget().screenGeometry()
dialog_geometry = self.frameGeometry()
center_point = screen_geometry.center()
dialog_geometry.moveCenter(center_point)
self.move(dialog_geometry.topLeft())
except Exception as e:
print(f"[Tour] Error centering dialog: {e}")
def _init_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
self.stacked_widget = QStackedWidget()
main_layout.addWidget(self.stacked_widget, 1)
# --- Define Tour Steps with Updated Content ---
step1_content = (
"Hello! This quick tour will walk you through the main features of the Kemono Downloader."
"<ul>"
"<li>Our goal is to help you easily download content from Kemono and Coomer.</li>"
"<li>Use the <b>Next</b> and <b>Back</b> buttons to navigate.</li>"
"<li>Click <b>Skip Tour</b> to close this guide at any time.</li>"
"<li>Check <b>'Never show this tour again'</b> if you don't want to see this on future startups.</li>"
"</ul>"
)
self.step1 = TourStepWidget("👋 Welcome!", step1_content)
step2_content = (
"Let's start with the basics for downloading:"
"<ul>"
"<li><b>🔗 Kemono Creator/Post URL:</b><br>"
" Paste the full web address (URL) of a creator's page (e.g., <i>https://kemono.su/patreon/user/12345</i>) "
"or a specific post (e.g., <i>.../post/98765</i>).</li><br>"
"<li><b>📁 Download Location:</b><br>"
" Click 'Browse...' to choose a folder on your computer where all downloaded files will be saved. "
"This is required unless you are using 'Only Links' mode.</li><br>"
"<li><b>📄 Page Range (Creator URLs only):</b><br>"
" If downloading from a creator's page, you can specify a range of pages (e.g., pages 2 to 5). "
"Leave blank for all pages. This is disabled for single post URLs or when <b>Manga/Comic Mode</b> is active.</li>"
"</ul>"
)
self.step2 = TourStepWidget("① Getting Started", step2_content)
step3_content = (
"Refine what you download with these filters:"
"<ul>"
"<li><b>🎯 Filter by Character(s):</b><br>"
" Enter character names, comma-separated (e.g., <i>Tifa, Aerith</i>). "
" <ul><li>In <b>Normal Mode</b>, this filters individual files by matching their filenames.</li>"
" <li>In <b>Manga/Comic Mode</b>, this filters entire posts by matching the post title. Useful for targeting specific series.</li>"
" <li>Also helps in folder naming if 'Separate Folders' is enabled.</li></ul></li><br>"
"<li><b>🚫 Skip with Words:</b><br>"
" Enter words, comma-separated (e.g., <i>WIP, sketch, preview</i>). "
" The <b>Scope</b> button (next to this input) cycles how this filter applies:"
" <ul><li><i>Scope: Files:</i> Skips files if their names contain any of these words.</li>"
" <li><i>Scope: Posts:</i> Skips entire posts if their titles contain any of these words.</li>"
" <li><i>Scope: Both:</i> Applies both file and post title skipping.</li></ul></li><br>"
"<li><b>Filter Files (Radio Buttons):</b> Choose what to download:"
" <ul>"
" <li><i>All:</i> Downloads all file types found.</li>"
" <li><i>Images/GIFs:</i> Only common image formats and GIFs.</li>"
" <li><i>Videos:</i> Only common video formats.</li>"
" <li><b><i>📦 Only Archives:</i></b> Exclusively downloads <b>.zip</b> and <b>.rar</b> files. When selected, 'Skip .zip' and 'Skip .rar' checkboxes are automatically disabled and unchecked.</li>"
" <li><i>🔗 Only Links:</i> Extracts and displays external links from post descriptions instead of downloading files.</li>"
" </ul></li>"
"</ul>"
)
self.step3 = TourStepWidget("② Filtering Downloads", step3_content)
step4_content = (
"More options to customize your downloads:"
"<ul>"
"<li><b>Skip .zip / Skip .rar:</b> Check these to avoid downloading these archive file types. "
" <i>(Note: These are disabled and ignored if '📦 Only Archives' mode is selected).</i></li><br>"
"<li><b>Download Thumbnails Only:</b> Downloads small preview images instead of full-sized files (if available).</li><br>"
"<li><b>Compress Large Images:</b> If the 'Pillow' library is installed, images larger than 1.5MB will be converted to WebP format if the WebP version is significantly smaller.</li><br>"
"<li><b>🗄️ Custom Folder Name (Single Post Only):</b><br>"
" If you are downloading a single specific post URL AND 'Separate Folders by Name/Title' is enabled, "
"you can enter a custom name here for that post's download folder.</li>"
"</ul>"
)
self.step4 = TourStepWidget("③ Fine-Tuning Downloads", step4_content)
step5_content = (
"Organize your downloads and manage performance:"
"<ul>"
"<li><b>⚙️ Separate Folders by Name/Title:</b> Creates subfolders based on the 'Filter by Character(s)' input or post titles (can use the 'Known Shows/Characters' list as a fallback for folder names).</li><br>"
"<li><b>Subfolder per Post:</b> If 'Separate Folders' is on, this creates an additional subfolder for <i>each individual post</i> inside the main character/title folder.</li><br>"
"<li><b>🚀 Use Multithreading (Threads):</b> Enables faster downloads for creator pages by processing multiple posts or files concurrently. The number of threads can be adjusted. Single post URLs are processed using a single thread for post data but can use multiple threads for file downloads within that post.</li><br>"
"<li><b>📖 Manga/Comic Mode (Creator URLs only):</b> Tailored for sequential content."
" <ul>"
" <li>Downloads posts from <b>oldest to newest</b>.</li>"
" <li>The 'Page Range' input is disabled as all posts are fetched.</li>"
" <li>A <b>filename style toggle button</b> (e.g., 'Name: Post Title' or 'Name: Original File') appears in the top-right of the log area when this mode is active for a creator feed. Click it to change naming:"
" <ul>"
" <li><b><i>Name: Post Title (Default):</i></b> The first file in a post is named after the post's title (e.g., <i>MyMangaChapter1.jpg</i>). Subsequent files in the <i>same post</i> (if any) will retain their original filenames.</li>"
" <li><b><i>Name: Original File:</i></b> All files will attempt to keep their original filenames as provided by the site (e.g., <i>001.jpg, page_02.png</i>). You'll see a recommendation to use 'Post Title' style if you choose this.</li>"
" </ul>"
" </li>"
" <li>For best results with 'Name: Post Title' style, use the 'Filter by Character(s)' field with the manga/series title.</li>"
" </ul></li><br>"
"<li><b>🎭 Known Shows/Characters:</b> Add names here (e.g., <i>Game Title, Series Name, Character Full Name</i>). These are used for automatic folder creation when 'Separate Folders' is on and no specific 'Filter by Character(s)' is provided for a post.</li>"
"</ul>"
)
self.step5 = TourStepWidget("④ Organization & Performance", step5_content)
step6_content = (
"Monitoring and Controls:"
"<ul>"
"<li><b>📜 Progress Log / Extracted Links Log:</b> Shows detailed download messages. If '🔗 Only Links' mode is active, this area displays the extracted links.</li><br>"
"<li><b>Show External Links in Log:</b> If checked, a secondary log panel appears below the main log to display any external links found in post descriptions. <i>(This is disabled if '🔗 Only Links' or '📦 Only Archives' mode is active).</i></li><br>"
"<li><b>Log Verbosity (Show Basic/Full Log):</b> Toggles the main log between showing all messages (Full) or only key summaries, errors, and warnings (Basic).</li><br>"
"<li><b>🔄 Reset:</b> Clears all input fields, logs, and resets temporary settings to their defaults. Can only be used when no download is active.</li><br>"
"<li><b>⬇️ Start Download / ❌ Cancel:</b> These buttons initiate or stop the current download/extraction process.</li>"
"</ul>"
"<br>You're all set! Click <b>'Finish'</b> to close the tour and start using the downloader."
)
self.step6 = TourStepWidget("⑤ Logs & Final Controls", step6_content)
self.tour_steps = [self.step1, self.step2, self.step3, self.step4, self.step5, self.step6]
for step_widget in self.tour_steps:
self.stacked_widget.addWidget(step_widget)
bottom_controls_layout = QVBoxLayout()
bottom_controls_layout.setContentsMargins(15, 10, 15, 15) # Adjusted margins
bottom_controls_layout.setSpacing(10)
self.never_show_again_checkbox = QCheckBox("Never show this tour again")
bottom_controls_layout.addWidget(self.never_show_again_checkbox, 0, Qt.AlignLeft)
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(10)
self.skip_button = QPushButton("Skip Tour")
self.skip_button.clicked.connect(self._skip_tour_action)
self.back_button = QPushButton("Back")
self.back_button.clicked.connect(self._previous_step)
self.back_button.setEnabled(False)
self.next_button = QPushButton("Next")
self.next_button.clicked.connect(self._next_step_action)
self.next_button.setDefault(True)
buttons_layout.addWidget(self.skip_button)
buttons_layout.addStretch(1)
buttons_layout.addWidget(self.back_button)
buttons_layout.addWidget(self.next_button)
bottom_controls_layout.addLayout(buttons_layout)
main_layout.addLayout(bottom_controls_layout)
self._update_button_states()
def _handle_exit_actions(self):
if self.never_show_again_checkbox.isChecked():
self.settings.setValue(self.TOUR_SHOWN_KEY, True)
self.settings.sync()
# else:
# print(f"[Tour] '{self.TOUR_SHOWN_KEY}' setting not set to True (checkbox was unchecked on exit).")
def _next_step_action(self):
if self.current_step < len(self.tour_steps) - 1:
self.current_step += 1
self.stacked_widget.setCurrentIndex(self.current_step)
else:
self._handle_exit_actions()
self.tour_finished_normally.emit()
self.accept()
self._update_button_states()
def _previous_step(self):
if self.current_step > 0:
self.current_step -= 1
self.stacked_widget.setCurrentIndex(self.current_step)
self._update_button_states()
def _skip_tour_action(self):
self._handle_exit_actions()
self.tour_skipped.emit()
self.reject()
def _update_button_states(self):
if self.current_step == len(self.tour_steps) - 1:
self.next_button.setText("Finish")
else:
self.next_button.setText("Next")
self.back_button.setEnabled(self.current_step > 0)
@staticmethod
def run_tour_if_needed(parent_app_window):
try:
settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
never_show_again = settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)
if never_show_again:
return QDialog.Rejected
tour_dialog = TourDialog(parent_app_window)
result = tour_dialog.exec_()
return result
except Exception as e:
print(f"[Tour] CRITICAL ERROR in run_tour_if_needed: {e}")
traceback.print_exc()
return QDialog.Rejected
if __name__ == '__main__':
app = QApplication(sys.argv)
# --- For testing: force the tour to show by resetting the flag ---
# print("[Tour Test] Resetting 'Never show again' flag for testing purposes.")
# test_settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
# test_settings.setValue(TourDialog.TOUR_SHOWN_KEY, False) # Set to False to force tour
# test_settings.sync()
# --- End testing block ---
print("[Tour Test] Running tour standalone...")
result = TourDialog.run_tour_if_needed(None)
if result == QDialog.Accepted:
print("[Tour Test] Tour dialog was accepted (Finished).")
elif result == QDialog.Rejected:
print("[Tour Test] Tour dialog was rejected (Skipped or previously set to 'Never show again').")
final_settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
print(f"[Tour Test] Final state of '{TourDialog.TOUR_SHOWN_KEY}' in settings: {final_settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)}")
sys.exit()