Skip to content

Commit c703560

Browse files
authored
Version 5.10.0 - Fast Mode, SQLite hash storage, and customizable font settings
2 parents 16afdf5 + ac6bf51 commit c703560

27 files changed

Lines changed: 4653 additions & 458 deletions

CHANGELOG

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
# Kemono Downloader Release Notes
22

3+
## v5.10.0 (10 February 2026)
4+
**feat: Fast Mode, SQLite hash storage, and customizable font settings**
5+
6+
- Add file size validation to HashDB for corruption detection
7+
Adds a `file_size` column to the SQLite hash database schema. When a file is successfully downloaded, its actual size on disk is now stored alongside the hash. On subsequent download attempts, the stored file size is compared against the existing file's size before performing the more expensive MD5 hash check. If a size mismatch is detected (indicating corruption or incomplete download), the file is automatically re-downloaded. Includes automatic schema migration for existing databases (ALTER TABLE) and backward compatibility for legacy JSON-migrated entries (default file_size of 0). Adds 7 new unit tests covering file_size storage, retrieval, corruption detection, legacy migration, and old-DB schema migration.
8+
9+
- Fix progress bars to accurately reflect download progress
10+
Resolves multiple issues causing progress bars to not reflect actual download state:
11+
- **Post Downloader**: Added `bool` success flag to `file_completed` signal (now `pyqtSignal(int, str, bool)`), matching Creator Downloader's existing pattern. Failed files now emit `file_completed(index, url, False)` instead of silently returning, ensuring they are counted in progress.
12+
- **Post Downloader**: Removed premature `file_completed` emission from inside the chunk download loop (was firing at progress == 100 before size validation completed).
13+
- **Both Downloaders**: Overall progress bar now calculates percentage as `(completed + failed) / total` instead of `completed / total`, so failed files no longer stall the progress bar.
14+
- **Post Downloader**: Added `failed_files` tracking set to the UI class with proper clearing alongside `completed_files` at all reset points.
15+
- **Post Downloader**: Download completion check now uses `completed + failed >= total` instead of `completed == total`.
16+
17+
- Add Fast Mode to Post Downloader and Creator Downloader
18+
Introduces a Fast Mode toggle (with qtawesome bolt icon) to both downloader tabs. When enabled, Fast Mode automatically selects all download categories, locks manual options to prevent accidental changes, and reveals a batch URL input area for pasting multiple URLs at once (one per line). Completed items are automatically removed from the queue. An info button (ℹ) explains the feature in a dialog. Includes full translations for all four languages and new unit tests.
19+
20+
- Fix Creator Downloader fast mode batch download
21+
In fast mode, pressing Download now auto-detects and downloads all posts for each queued creator sequentially instead of requiring manual post selection. Uses a pipeline of `_fast_mode_process_next()` → PostDetectionThread → `_fast_mode_auto_download()` → `prepare_files_for_download()` to process each creator URL and advance to the next on completion.
22+
23+
- Lock UI controls during active downloads
24+
All UI elements (inputs, checkboxes, buttons, queue lists, pagination, other tabs, and settings tab) are disabled during a download in both Post and Creator downloader tabs. Only the Cancel button and Expand Logs remain enabled. New `set_downloading_ui_state()` method in both tabs provides comprehensive lock/unlock behavior.
25+
26+
- Fix Expand Logs window freezing
27+
Both Post and Creator downloader LogsWindow dialogs now use QTimer-based batched updates (500ms interval) instead of synchronous HTML copying on every log entry, preventing UI freezes during heavy download logging.
28+
29+
- Migrate file deduplication from JSON to SQLite
30+
Replaces the `file_hashes.json` flat-file backend with a new `hash_db.py` module backed by SQLite (WAL mode, thread-safe). The `HashDB` class provides `has()`, `add()`, `remove()`, and `all_hashes()` methods. Existing JSON hash files are automatically migrated on first access. Improves performance and reliability for large hash sets. Includes 16 unit tests covering CRUD, migration, and thread safety.
31+
32+
- Add font settings to the Settings tab
33+
Introduces a new Font Settings group in the settings UI, allowing users to switch the application-wide font between JetBrains Mono (default) and Poppins. Fonts are bundled as TTF files (Regular, Bold, Medium weights) and loaded at startup via QFontDatabase. The selected font is persisted via QSettings and applied across the entire application, including the intro screen, help tab, and browser extension tab. Adds `font_changed` signal to SettingsTab, dynamic `_get_font_family()` helpers in HelpTab and ExtensionTab, recursive font propagation in `KemonoDownloader.apply_font()`, and translations for the font UI in all four languages (English, Japanese, Korean, Chinese-Simplified). Includes 39 new unit tests covering defaults, combo box, signal emission, dialog messages, reset, bundled files, translations, QSettings persistence, and UI elements.
34+
35+
- **fix:** Replace `locale.getlocale(locale.LC_ALL)` with `locale.getlocale(locale.LC_CTYPE)` in downloader modules to prevent `TypeError` on Python 3.14/CachyOS; ensures locale detection works across all OSes and Python versions.
36+
37+
- Include PyQt BaseApp in Flatpak config to provide `libgssapi_krb5.so.2` and prevent runtime crashes
38+
39+
340
## v5.9.0 (09 February 2026)
441
**feat: Creator filename/folder customization, browser extension, and misc fixes**
542
- Add creator filename/folder customization

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey" alt="Platforms">
4141
</a>
4242
<a href="https://github.com/VoxDroid/KemonoDownloader/releases">
43-
<img src="https://img.shields.io/badge/version-v5.9.0-brightgreen" alt="Version">
43+
<img src="https://img.shields.io/badge/version-v5.10.0-brightgreen" alt="Version">
4444
</a>
4545
<a>
4646
<img src="https://img.shields.io/github/v/release/VoxDroid/KemonoDownloader?label=Latest%20Release" alt="Latest Release">
@@ -158,14 +158,16 @@ KemonoDownloader offers a comprehensive set of features designed to efficiently
158158
| **Creator Downloader** | Bulk download entire creator profiles or selected posts. Configurable options for main files, attachments, and content images. |
159159
| **File Type Support** | Handles images (JPG, PNG, GIF, WebP), videos (MP4, AVI, MOV), archives (ZIP, 7Z, RAR), documents (PDF, TXT), audio (MP3, WAV), and more. |
160160
| **URL Import** | Import multiple creator URLs from .txt files for batch processing. |
161+
| **Fast Mode** | One-click toggle that selects all file categories, locks options, and enables batch URL input for rapid bulk downloading. In Creator Downloader, auto-detects and downloads all posts for each queued creator sequentially. |
162+
| **Download UI Lock** | All controls are disabled during active downloads (except Cancel and Expand Logs) to prevent accidental changes. |
161163

162164
### $\color{#90a4ae}{\sf{\text{Performance and Reliability}}}$
163165

164166
| Feature | Description |
165167
|---------|-------------|
166168
| **Concurrent Downloads** | Adjustable parallel downloads (1-10 threads) for optimal performance. |
167169
| **Retry Mechanisms** | Configurable retries for posts fetching, data retrieval, file downloads, and API requests. |
168-
| **File Deduplication** | Prevents redundant downloads using URL-based hashing. |
170+
| **File Deduplication** | Prevents redundant downloads using SQLite-backed URL hashing (auto-migrates from legacy JSON). |
169171
| **Connection Pooling** | Efficient HTTP connection management with gzip compression support. |
170172

171173
### $\color{#90a4ae}{\sf{\text{User Interface and Experience}}}$
@@ -202,6 +204,11 @@ KemonoDownloader offers a comprehensive set of features designed to efficiently
202204
- **Folder Name**: Configurable app data folder name
203205
- **Platform Defaults**: Auto-detection of appropriate directories per OS
204206

207+
#### $\color{#90a4ae}{\sf{\text{Font Settings}}}$
208+
| Setting | Description | Default | Options |
209+
|---------|-------------|---------|---------|
210+
| Font | Application-wide font family (bundled Google Fonts) | JetBrains Mono | JetBrains Mono, Poppins |
211+
205212
### $\color{#90a4ae}{\sf{\text{Help and Documentation}}}$
206213

207214
| Feature | Description |

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.briefcase]
22
project_name = "Kemono Downloader"
33
bundle = "com.voxdroid"
4-
version = "5.9.0"
4+
version = "5.10.0"
55
url = "https://github.com/VoxDroid/KemonoDownloader"
66
license.file = "LICENSE"
77
author = "VoxDroid"
@@ -122,6 +122,8 @@ finish_arg."filesystem=home" = true
122122
flatpak_runtime = "org.kde.Platform"
123123
flatpak_runtime_version = "6.7"
124124
flatpak_sdk = "org.kde.Sdk"
125+
flatpak_base = "com.riverbankcomputing.PyQt.BaseApp"
126+
flatpak_base_version = "6.7"
125127

126128
[tool.briefcase.app.kemonodownloader.windows]
127129
requires = [

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ pre-commit
1414
mypy
1515
pyright
1616
pytest
17-
pytest-qt
17+
pytest-qt
18+
chardet<6

src/kemonodownloader/app.py

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from bs4 import MarkupResemblesLocatorWarning
1010
from packaging import version
1111
from PyQt6.QtCore import QEasingCurve, QPropertyAnimation, Qt, QThread, pyqtSignal
12-
from PyQt6.QtGui import QColor, QCursor, QFont, QIcon, QPalette, QPixmap
12+
from PyQt6.QtGui import QColor, QCursor, QFont, QFontDatabase, QIcon, QPalette, QPixmap
1313
from PyQt6.QtWidgets import (
1414
QApplication,
1515
QGraphicsDropShadowEffect,
@@ -32,9 +32,33 @@
3232

3333
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
3434

35-
CURRENT_VERSION = "5.9.0"
35+
CURRENT_VERSION = "5.10.0"
3636
GITHUB_REPO = "VoxDroid/KemonoDownloader"
3737

38+
# Available Google Fonts bundled with the app
39+
BUNDLED_FONTS = {
40+
"JetBrains Mono": [
41+
"JetBrainsMono-Regular.ttf",
42+
"JetBrainsMono-Bold.ttf",
43+
"JetBrainsMono-Medium.ttf",
44+
],
45+
"Poppins": [
46+
"Poppins-Regular.ttf",
47+
"Poppins-Bold.ttf",
48+
"Poppins-Medium.ttf",
49+
],
50+
}
51+
52+
53+
def load_bundled_fonts():
54+
"""Load all bundled Google Fonts into the application font database."""
55+
fonts_dir = os.path.join(os.path.dirname(__file__), "resources", "fonts")
56+
for font_family, font_files in BUNDLED_FONTS.items():
57+
for font_file in font_files:
58+
font_path = os.path.join(fonts_dir, font_file)
59+
if os.path.exists(font_path):
60+
QFontDatabase.addApplicationFont(font_path)
61+
3862

3963
class VersionChecker(QThread):
4064
update_available = pyqtSignal(str, str)
@@ -94,7 +118,7 @@ def setup_ui(self):
94118

95119
# Version Label
96120
self.version_label = QLabel(f"Version {CURRENT_VERSION}")
97-
self.version_label.setFont(QFont("Poppins", 12))
121+
self.version_label.setFont(QFont(self._get_font_family(), 12))
98122
self.version_label.setStyleSheet("color: #CCCCCC; background: transparent;")
99123
self.version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
100124
main_layout.addWidget(
@@ -103,8 +127,11 @@ def setup_ui(self):
103127

104128
# Launch Button
105129
self.launch_button = QPushButton(translate("launch_button"))
106-
self.launch_button.setFont(QFont("Poppins", 16, QFont.Weight.Medium))
107-
self.launch_button.setFixedSize(220, 60)
130+
self.launch_button.setFont(
131+
QFont(self._get_font_family(), 16, QFont.Weight.Medium)
132+
)
133+
# larger width for launch button
134+
self.launch_button.setFixedSize(300, 60)
108135
self.launch_button.setStyleSheet(
109136
"""
110137
QPushButton {
@@ -141,21 +168,21 @@ def setup_ui(self):
141168
footer_layout.setContentsMargins(0, 0, 0, 0)
142169

143170
self.title = QLabel(translate("app_title"))
144-
self.title.setFont(QFont("Poppins", 14, QFont.Weight.Bold))
171+
self.title.setFont(QFont(self._get_font_family(), 14, QFont.Weight.Bold))
145172
self.title.setStyleSheet("color: #FFFFFF; background: transparent;")
146173
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
147174
footer_layout.addWidget(self.title)
148175

149176
self.dev_label = QLabel(translate("developed_by"))
150-
self.dev_label.setFont(QFont("Poppins", 10))
177+
self.dev_label.setFont(QFont(self._get_font_family(), 10))
151178
self.dev_label.setStyleSheet("color: #CCCCCC; background: transparent;")
152179
self.dev_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
153180
footer_layout.addWidget(self.dev_label)
154181

155182
self.github_label = QLabel(
156183
'<a href="https://github.com/VoxDroid" style="color: #A0C0FF; text-decoration: none; font-size: 10px;">github.com/VoxDroid</a>'
157184
)
158-
self.github_label.setFont(QFont("Poppins", 10))
185+
self.github_label.setFont(QFont(self._get_font_family(), 10))
159186
self.github_label.setOpenExternalLinks(True)
160187
self.github_label.setStyleSheet(
161188
"QLabel { background: transparent; } QLabel:hover { color: #C0E0FF; }"
@@ -166,6 +193,18 @@ def setup_ui(self):
166193

167194
main_layout.addWidget(footer_widget, alignment=Qt.AlignmentFlag.AlignCenter)
168195

196+
def _get_font_family(self):
197+
"""Get the current font family from settings."""
198+
return self._parent.settings_tab.get_font()
199+
200+
def apply_font(self, font_family: str):
201+
"""Update all fonts in the intro screen to use the new font family."""
202+
self.version_label.setFont(QFont(font_family, 12))
203+
self.launch_button.setFont(QFont(font_family, 16, QFont.Weight.Medium))
204+
self.title.setFont(QFont(font_family, 14, QFont.Weight.Bold))
205+
self.dev_label.setFont(QFont(font_family, 10))
206+
self.github_label.setFont(QFont(font_family, 10))
207+
169208
def update_ui_text(self):
170209
self.title.setText(translate("app_title"))
171210
self.dev_label.setText(translate("developed_by"))
@@ -214,10 +253,46 @@ def __init__(self):
214253
self.apply_palette()
215254

216255
self.settings_tab.language_changed.connect(self.update_all_ui)
256+
self.settings_tab.font_changed.connect(self.apply_font)
257+
258+
# Apply the saved font setting
259+
self.apply_font(self.settings_tab.get_font())
217260

218261
if self.settings_tab.is_auto_check_updates_enabled():
219262
self.check_for_updates()
220263

264+
def apply_font(self, font_family: str):
265+
"""Apply the selected font family to the entire application and all widgets."""
266+
app = QApplication.instance()
267+
if app:
268+
font = QFont(font_family)
269+
font.setPointSize(app.font().pointSize())
270+
app.setFont(font)
271+
# Update all existing widgets that have explicit fonts set
272+
self._apply_font_recursive(self, font_family)
273+
# Update the intro screen if it still exists
274+
if hasattr(self, "intro_screen") and self.intro_screen is not None:
275+
try:
276+
self.intro_screen.apply_font(font_family)
277+
except RuntimeError:
278+
# C++ object already deleted after intro-to-main transition
279+
self.intro_screen = None
280+
# Refresh help and extension tabs if they exist
281+
if hasattr(self, "help_tab"):
282+
self.help_tab.update_ui_text()
283+
if hasattr(self, "extension_tab"):
284+
self.extension_tab.update_ui_text()
285+
286+
def _apply_font_recursive(self, widget, font_family: str):
287+
"""Recursively update the font family on all child widgets."""
288+
current_font = widget.font()
289+
current_font.setFamily(font_family)
290+
widget.setFont(current_font)
291+
for child in widget.findChildren(QWidget):
292+
child_font = child.font()
293+
child_font.setFamily(font_family)
294+
child.setFont(child_font)
295+
221296
def apply_palette(self):
222297
palette = QPalette()
223298
palette.setColor(QPalette.ColorRole.Window, QColor("#1A2A44"))
@@ -391,12 +466,15 @@ def transition_to_main(self):
391466
self.main_fade.setEndValue(1)
392467
self.main_fade.setEasingCurve(QEasingCurve.Type.InOutQuad)
393468

394-
self.intro_fade.finished.connect(
395-
lambda: self.setCentralWidget(self.main_widget)
396-
)
469+
self.intro_fade.finished.connect(self._finish_intro_transition)
397470
self.intro_fade.start()
398471
self.main_fade.start()
399472

473+
def _finish_intro_transition(self):
474+
"""Complete the intro-to-main transition and release the intro screen."""
475+
self.setCentralWidget(self.main_widget)
476+
self.intro_screen = None
477+
400478
def check_for_updates(self):
401479
self.version_checker = VersionChecker()
402480
self.version_checker.update_available.connect(self.show_update_notification)
@@ -507,6 +585,7 @@ def log(self, message):
507585
def main():
508586
app = QApplication(sys.argv)
509587
app.setStyle("Fusion")
588+
load_bundled_fonts()
510589
window = KemonoDownloader()
511590
window.show()
512591
sys.exit(app.exec())

0 commit comments

Comments
 (0)