novelfetch 2.0.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- novelfetch-2.0.9/LICENSE +21 -0
- novelfetch-2.0.9/PKG-INFO +256 -0
- novelfetch-2.0.9/README.md +227 -0
- novelfetch-2.0.9/core/__init__.py +5 -0
- novelfetch-2.0.9/core/downloader.py +168 -0
- novelfetch-2.0.9/core/epub.py +143 -0
- novelfetch-2.0.9/core/http_client.py +262 -0
- novelfetch-2.0.9/core/library.py +423 -0
- novelfetch-2.0.9/core/paths.py +69 -0
- novelfetch-2.0.9/core/progress.py +201 -0
- novelfetch-2.0.9/core/translation.py +93 -0
- novelfetch-2.0.9/core/utils.py +83 -0
- novelfetch-2.0.9/gui/__init__.py +1 -0
- novelfetch-2.0.9/gui/async_runner.py +100 -0
- novelfetch-2.0.9/gui/data/NotoNaskhArabic.ttf +0 -0
- novelfetch-2.0.9/gui/data/icon.png +0 -0
- novelfetch-2.0.9/gui/kv/chapter_list.kv +86 -0
- novelfetch-2.0.9/gui/kv/download_dialog.kv +47 -0
- novelfetch-2.0.9/gui/kv/download_picker.kv +62 -0
- novelfetch-2.0.9/gui/kv/history.kv +50 -0
- novelfetch-2.0.9/gui/kv/home_tab.kv +208 -0
- novelfetch-2.0.9/gui/kv/novel_list.kv +44 -0
- novelfetch-2.0.9/gui/kv/reader.kv +71 -0
- novelfetch-2.0.9/gui/kv/search_tab.kv +121 -0
- novelfetch-2.0.9/gui/kv/settings_tab.kv +126 -0
- novelfetch-2.0.9/gui/kv/topbar.kv +24 -0
- novelfetch-2.0.9/gui/kv/update.kv +60 -0
- novelfetch-2.0.9/gui/main.py +154 -0
- novelfetch-2.0.9/gui/novelfetch.kv +11 -0
- novelfetch-2.0.9/gui/screens/__init__.py +1 -0
- novelfetch-2.0.9/gui/screens/app_settings.py +82 -0
- novelfetch-2.0.9/gui/screens/browse.py +162 -0
- novelfetch-2.0.9/gui/screens/chapter_list.py +540 -0
- novelfetch-2.0.9/gui/screens/download_dialog.py +133 -0
- novelfetch-2.0.9/gui/screens/download_picker.py +195 -0
- novelfetch-2.0.9/gui/screens/history.py +212 -0
- novelfetch-2.0.9/gui/screens/home_tab.py +1593 -0
- novelfetch-2.0.9/gui/screens/main_screen.py +134 -0
- novelfetch-2.0.9/gui/screens/novel_list.py +43 -0
- novelfetch-2.0.9/gui/screens/reader.py +660 -0
- novelfetch-2.0.9/gui/screens/search_tab.py +278 -0
- novelfetch-2.0.9/gui/screens/settings_tab.py +345 -0
- novelfetch-2.0.9/gui/screens/source_picker.py +73 -0
- novelfetch-2.0.9/gui/screens/theme.py +63 -0
- novelfetch-2.0.9/gui/screens/topbar.py +72 -0
- novelfetch-2.0.9/gui/screens/update.py +490 -0
- novelfetch-2.0.9/gui/screens/utils.py +811 -0
- novelfetch-2.0.9/novelfetch.egg-info/PKG-INFO +256 -0
- novelfetch-2.0.9/novelfetch.egg-info/SOURCES.txt +71 -0
- novelfetch-2.0.9/novelfetch.egg-info/dependency_links.txt +1 -0
- novelfetch-2.0.9/novelfetch.egg-info/entry_points.txt +3 -0
- novelfetch-2.0.9/novelfetch.egg-info/requires.txt +19 -0
- novelfetch-2.0.9/novelfetch.egg-info/top_level.txt +4 -0
- novelfetch-2.0.9/pyproject.toml +110 -0
- novelfetch-2.0.9/setup.cfg +4 -0
- novelfetch-2.0.9/sources/__init__.py +14 -0
- novelfetch-2.0.9/sources/base.py +93 -0
- novelfetch-2.0.9/sources/novelfire.py +266 -0
- novelfetch-2.0.9/sources/novelphoenix.py +295 -0
- novelfetch-2.0.9/sources/royalroad.py +158 -0
- novelfetch-2.0.9/sources/scriblehub.py +264 -0
- novelfetch-2.0.9/sources/wuxiaspot.py +328 -0
- novelfetch-2.0.9/tui/__init__.py +12 -0
- novelfetch-2.0.9/tui/browse.py +340 -0
- novelfetch-2.0.9/tui/download.py +447 -0
- novelfetch-2.0.9/tui/library.py +223 -0
- novelfetch-2.0.9/tui/main.py +46 -0
- novelfetch-2.0.9/tui/main_menu.py +123 -0
- novelfetch-2.0.9/tui/novelfetch.tcss +128 -0
- novelfetch-2.0.9/tui/reader.py +264 -0
- novelfetch-2.0.9/tui/shared.py +112 -0
- novelfetch-2.0.9/tui/source_picker.py +37 -0
- novelfetch-2.0.9/tui/utils.py +3 -0
novelfetch-2.0.9/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohamed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: novelfetch
|
|
3
|
+
Version: 2.0.9
|
|
4
|
+
Summary: A novel fetching and reading application with TUI and Android (KivyMD) frontends
|
|
5
|
+
Author: Mohamed
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/Momokh99/NovelFetch
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: httpx>=0.28.0
|
|
12
|
+
Requires-Dist: beautifulsoup4>=4.14.0
|
|
13
|
+
Requires-Dist: lxml>=4.9.0
|
|
14
|
+
Requires-Dist: deep-translator>=1.11.0
|
|
15
|
+
Requires-Dist: EbookLib>=0.20
|
|
16
|
+
Requires-Dist: textual>=8.0.0
|
|
17
|
+
Requires-Dist: curl_cffi>=0.15.0
|
|
18
|
+
Provides-Extra: gui
|
|
19
|
+
Requires-Dist: kivy>=2.3.0; extra == "gui"
|
|
20
|
+
Requires-Dist: kivymd>=2.0.0; extra == "gui"
|
|
21
|
+
Requires-Dist: arabic-reshaper; extra == "gui"
|
|
22
|
+
Requires-Dist: python-bidi; extra == "gui"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: ruff>=0.8.0; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.13; extra == "dev"
|
|
26
|
+
Requires-Dist: pre-commit>=4.0; extra == "dev"
|
|
27
|
+
Requires-Dist: watchdog>=5.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# NovelFetch
|
|
31
|
+
|
|
32
|
+
A terminal + mobile novel reader and downloader with a pluggable scraper architecture. Browse, search, download, translate, and read chapters offline. Ships two frontends over one shared framework: a **Textual TUI** for the terminal and a **KivyMD GUI** for desktop + Android.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
███╗ ██╗ ██████╗ ██╗ ██╗███████╗██╗ ██████╗ ██╗███╗ ██╗
|
|
36
|
+
████╗ ██║██╔═══██╗██║ ██║██╔════╝██║ ██╔══██╗██║████╗ ██║
|
|
37
|
+
██╔██╗ ██║██║ ██║██║ ██║█████╗ ██║ ██████╔╝██║██╔██╗ ██║
|
|
38
|
+
██║╚██╗██║██║ ██║╚██╗ ██╔╝██╔══╝ ██║ ██╔══██╗██║██║╚██╗██║
|
|
39
|
+
██║ ╚████║╚██████╔╝ ╚████╔╝ ███████╗███████╗██████╔╝██║██║ ╚████║
|
|
40
|
+
╚═╝ ╚═══╝ ╚═════╝ ╚═══╝ ╚══════╝╚══════╝╚═════╝ ╚═╝╚═╝ ╚═══╝
|
|
41
|
+
════════════════════════════════════════════════════════════════════
|
|
42
|
+
TUI + GUI Novel Reader v2
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<img src="docs/images/tui-main-menu.png" alt="NovelFetch Textual TUI main menu — the active source's ASCII banner above Primary actions (Search by name, My Library) and Browse lists (Hot novels, Latest releases, Most popular, Completed novels, Browse by genre)" width="800">
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+

|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- **Search** — real-time search with 750ms debounce; paginate with `n`/`p`
|
|
58
|
+
- **Browse** — hot, latest, most popular, completed, and dozens of genres per source
|
|
59
|
+
- **My Library** — browse locally downloaded novels, resume reading, delete
|
|
60
|
+
- **Reader** — next/prev (`n`/`p`), jump to chapter (`j`), download current (`d`)
|
|
61
|
+
- **Progress tracking** — auto-saves last chapter; ✓ marks read chapters
|
|
62
|
+
- **Download dialog** — All, Range, or Translated; progress bar with translation warning
|
|
63
|
+
- **Translation** — Google Translate (12 languages); Arabic RTL layout with shaped text
|
|
64
|
+
- **EPUB export** — generate EPUB files from downloaded chapters
|
|
65
|
+
- **Delete with safety** — double-press `x` to confirm deletion
|
|
66
|
+
|
|
67
|
+
Key bindings in the reader:
|
|
68
|
+
|
|
69
|
+
| Key | Action |
|
|
70
|
+
| ----- | -------- |
|
|
71
|
+
| `n` / `p` | Next / Prev chapter |
|
|
72
|
+
| `j` | Jump to chapter number |
|
|
73
|
+
| `d` | Download dialog (All / Range / Translated) |
|
|
74
|
+
| `t` | Translate to language |
|
|
75
|
+
| `r` | Revert to original text |
|
|
76
|
+
| `h` | Go to main menu |
|
|
77
|
+
| `q` | Back to chapter list |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Two frontends, one core
|
|
82
|
+
|
|
83
|
+
- `main.py` — entry-point dispatcher: TUI on desktop, GUI on Android (`ANDROID_ARGUMENT`).
|
|
84
|
+
- `tui/` — **Textual TUI**: `main.py` (app), `main_menu.py`, `browse.py`, `reader.py`, `library.py`, `download.py`, `source_picker.py`, `shared.py`, `utils.py`, `novelfetch.tcss`.
|
|
85
|
+
- `gui/` — **KivyMD GUI** (desktop + Android): `main.py`, `screens/` (`main_screen`, `home_tab`, `search_tab`, `update`, `history`, `settings_tab`, `novel_list`, `chapter_list`, `reader`, `download_picker`, `download_dialog`, `source_picker`, `topbar`, `theme`, `app_settings`), `kv/` layout files, `data/` (icon, bundled Arabic font).
|
|
86
|
+
- `core/` — shared framework: `progress.py`, `translation.py`, `epub.py`, `downloader.py`, `http_client.py`, `library.py`, `utils.py` (source dispatch), `paths.py` (data-dir resolution for frozen/AppImage/Android).
|
|
87
|
+
- `sources/` — pluggable `Source` interface (`base.py`) and a `REGISTRY` of scrapers.
|
|
88
|
+
- `novels/` — downloaded chapters, per-novel `meta.json` / `cover.*`, `progress.json`, `tracking.json`.
|
|
89
|
+
|
|
90
|
+
## Pluggable sources
|
|
91
|
+
|
|
92
|
+
The `Source` abstraction (`sources/base.py:1`) defines browsing, search, chapter fetching, and cover resolution; implementations are registered in `sources/__init__.py:7`:
|
|
93
|
+
|
|
94
|
+
| Source | Browse lists | Genres | Notes |
|
|
95
|
+
|--------|-------------|--------|-------|
|
|
96
|
+
| **NovelFire** | 9 | 47 | Throttled paging (`PAGE_DELAY = 0.4`) — novelfire.net returns 429 on burst paging |
|
|
97
|
+
| **NovelPhoenix** | 9 | 55 | Condensed 79-column ASCII banner so it doesn't wrap |
|
|
98
|
+
| **RoyalRoad** | 8 (hot, popular, latest, newest, completed, rising stars, ongoing, more) | 13 | AJAX-free chapter table |
|
|
99
|
+
| **ScribbleHub** | 4 (hot, latest, popular, completed) | 25 | Cloudflare-aware; lazy `curl_cffi` with httpx fallback (p4a-safe) |
|
|
100
|
+
| **WuxiaSpot** | 7 | 48 | AJAX pagination (`fy.php`); search marked unreliable |
|
|
101
|
+
|
|
102
|
+
New sources implement the same interface and register a key — no frontend changes needed.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Installation
|
|
107
|
+
|
|
108
|
+
### From PyPI
|
|
109
|
+
|
|
110
|
+
Requires **Python 3.10+**.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip install novelfetch # terminal app (TUI)
|
|
114
|
+
novelfetch-tui
|
|
115
|
+
|
|
116
|
+
pip install "novelfetch[gui]" # + KivyMD desktop GUI
|
|
117
|
+
novelfetch-gui
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The TUI is the primary frontend, so its dependencies (`textual`, `curl_cffi`) are
|
|
121
|
+
installed by default and `pip install novelfetch` works out of the box. The KivyMD
|
|
122
|
+
GUI is opt-in because it pulls in Kivy — use the `[gui]` extra for the desktop
|
|
123
|
+
frontend, or the APK for Android.
|
|
124
|
+
|
|
125
|
+
Note that installing puts the top-level modules `core`, `tui`, `gui`, and `sources`
|
|
126
|
+
in `site-packages`. They are generic names and can collide with unrelated packages;
|
|
127
|
+
if you hit an import conflict, install into a dedicated virtualenv.
|
|
128
|
+
|
|
129
|
+
User data (novels, reading progress, settings) lives in `~/.novelfetch`, or
|
|
130
|
+
`$NOVELFETCH_DATA_DIR` if you set it.
|
|
131
|
+
|
|
132
|
+
### From source
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
git clone https://github.com/Momokh99/NovelFetch.git
|
|
136
|
+
cd NovelFetch
|
|
137
|
+
make setup # creates both environments (TUI + GUI)
|
|
138
|
+
make pre-commit-install
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Running from source
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
make run-tui # Textual TUI (terminal)
|
|
145
|
+
make run-kivy # KivyMD GUI (desktop)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### What `make setup` does
|
|
149
|
+
|
|
150
|
+
The project uses two separate virtual environments:
|
|
151
|
+
|
|
152
|
+
| Environment | Purpose | Created by |
|
|
153
|
+
|-------------|---------|------------|
|
|
154
|
+
| `myenv/` | TUI + code quality tools (Ruff, mypy, Pyright) | `make setup-tui` |
|
|
155
|
+
| `android_env/` | KivyMD GUI (desktop + Android) | `make setup-android` |
|
|
156
|
+
|
|
157
|
+
Dependencies are split across `pyproject.toml` optional groups:
|
|
158
|
+
- **Core** (always installed): `httpx`, `beautifulsoup4`, `lxml`, `deep-translator`, `ebooklib`, `textual`, `curl_cffi`
|
|
159
|
+
- **GUI** (`pip install "novelfetch[gui]"`): `kivy`, `kivymd`, `arabic-reshaper`, `python-bidi`
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## How It Works
|
|
164
|
+
|
|
165
|
+
**Architecture:**
|
|
166
|
+
|
|
167
|
+
The package layout is described once, in [Two frontends, one core](#two-frontends-one-core) above — `main.py` dispatches to either `tui/` or `gui/`, both of which sit on `core/` and `sources/`.
|
|
168
|
+
|
|
169
|
+
The GUI is the richer frontend: a 5-tab `MDNavigationBar` (Home / Search / Updates / History / Settings) plus per-novel browse flow. Settings persist to `app_settings.json`; update checks reconcile against `update_results.json`. Downloads run concurrently (`core/downloader.py` caps at 4) with a live progress bar.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Android packaging
|
|
174
|
+
|
|
175
|
+
`buildozer.spec` builds a debug APK with python-for-android:
|
|
176
|
+
|
|
177
|
+
- arm64-v8a, API 34 (min 21), NDK 25b
|
|
178
|
+
- p4a pinned to `v2026.05.09` (avoids nightly-master breakage with pip ≥ 26)
|
|
179
|
+
- CI (`release.yml`) builds and publishes the APK with every tagged release
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
make android-debug # build debug APK
|
|
183
|
+
make android-deploy # deploy to connected device
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Roadmap
|
|
189
|
+
|
|
190
|
+
- [x] TUI mode (Textual interface)
|
|
191
|
+
- [x] GUI mode (KivyMD desktop + Android)
|
|
192
|
+
- [x] Resume from last chapter (progress.json)
|
|
193
|
+
- [x] Search with auto-type and pagination
|
|
194
|
+
- [x] Translation (Google Translate, 12 languages, RTL support)
|
|
195
|
+
- [x] Multi-source architecture (NovelFire, NovelPhoenix, RoyalRoad, ScribbleHub, WuxiaSpot)
|
|
196
|
+
- [x] Download dialog (All, Range, Translated)
|
|
197
|
+
- [x] Offline reading mode
|
|
198
|
+
- [x] Reading history across sessions
|
|
199
|
+
- [x] EPUB export
|
|
200
|
+
- [x] APK build via Buildozer
|
|
201
|
+
- [x] PyPI distribution (TUI by default, GUI via the `[gui]` extra)
|
|
202
|
+
- [ ] Better text formatting (italics, line breaks, spacing)
|
|
203
|
+
- [ ] Search filters (genre, status, rating)
|
|
204
|
+
- [ ] Adding to AUR — blocked: AUR account registration disabled upstream; packages ready to push once sign-ups reopen
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
### Quick Start
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# One-time setup
|
|
214
|
+
make setup
|
|
215
|
+
make pre-commit-install
|
|
216
|
+
|
|
217
|
+
# Daily development
|
|
218
|
+
make run-tui # Run the TUI app
|
|
219
|
+
make run-kivy # Run the KivyMD GUI app
|
|
220
|
+
make lint # Check code quality
|
|
221
|
+
make format # Auto-format code
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Available Commands
|
|
225
|
+
|
|
226
|
+
| Command | Description |
|
|
227
|
+
|---------|-------------|
|
|
228
|
+
| `make setup` | Create both venvs and install all dependencies |
|
|
229
|
+
| `make run-tui` | Run TUI app (myenv) |
|
|
230
|
+
| `make run-kivy` | Run KivyMD GUI app (android_env) |
|
|
231
|
+
| `make lint` | Run Ruff + mypy + Pyright |
|
|
232
|
+
| `make lint-fix` | Auto-fix lint issues |
|
|
233
|
+
| `make format` | Auto-format code (ruff format + import sorting) |
|
|
234
|
+
| `make format-check` | Check formatting without modifying |
|
|
235
|
+
| `make android-debug` | Build debug APK |
|
|
236
|
+
| `make android-release` | Build release APK |
|
|
237
|
+
| `make android-deploy` | Deploy to connected device |
|
|
238
|
+
| `make android-logcat` | Show device logcat |
|
|
239
|
+
| `make android-clean` | Clean buildozer build artifacts |
|
|
240
|
+
| `make pre-commit-run` | Run pre-commit on all files |
|
|
241
|
+
| `make clean` | Remove build artifacts and caches |
|
|
242
|
+
| `make bump-release VERSION=x.y.z` | Bump version in pyproject.toml + buildozer.spec |
|
|
243
|
+
| `make pkg` | Build the wheel + sdist into `dist/` and check their metadata |
|
|
244
|
+
| `make pkg-publish` | Upload `dist/*` to PyPI (manual fallback for the publish workflow) |
|
|
245
|
+
|
|
246
|
+
### Development Tools
|
|
247
|
+
|
|
248
|
+
- **Linting**: Ruff locally via `make lint`; CI keeps a flake8 syntax/undefined-name gate
|
|
249
|
+
- **Type Checking**: Mypy + Pyright
|
|
250
|
+
- **Pre-commit**: Auto-format on commit
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Disclaimer
|
|
255
|
+
|
|
256
|
+
This tool scrapes publicly available content for personal use only. Novels belong to their authors and translators. Do not redistribute downloaded content. Support the creators if you enjoy their work.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# NovelFetch
|
|
2
|
+
|
|
3
|
+
A terminal + mobile novel reader and downloader with a pluggable scraper architecture. Browse, search, download, translate, and read chapters offline. Ships two frontends over one shared framework: a **Textual TUI** for the terminal and a **KivyMD GUI** for desktop + Android.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
███╗ ██╗ ██████╗ ██╗ ██╗███████╗██╗ ██████╗ ██╗███╗ ██╗
|
|
7
|
+
████╗ ██║██╔═══██╗██║ ██║██╔════╝██║ ██╔══██╗██║████╗ ██║
|
|
8
|
+
██╔██╗ ██║██║ ██║██║ ██║█████╗ ██║ ██████╔╝██║██╔██╗ ██║
|
|
9
|
+
██║╚██╗██║██║ ██║╚██╗ ██╔╝██╔══╝ ██║ ██╔══██╗██║██║╚██╗██║
|
|
10
|
+
██║ ╚████║╚██████╔╝ ╚████╔╝ ███████╗███████╗██████╔╝██║██║ ╚████║
|
|
11
|
+
╚═╝ ╚═══╝ ╚═════╝ ╚═══╝ ╚══════╝╚══════╝╚═════╝ ╚═╝╚═╝ ╚═══╝
|
|
12
|
+
════════════════════════════════════════════════════════════════════
|
|
13
|
+
TUI + GUI Novel Reader v2
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="docs/images/tui-main-menu.png" alt="NovelFetch Textual TUI main menu — the active source's ASCII banner above Primary actions (Search by name, My Library) and Browse lists (Hot novels, Latest releases, Most popular, Completed novels, Browse by genre)" width="800">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+

|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Search** — real-time search with 750ms debounce; paginate with `n`/`p`
|
|
29
|
+
- **Browse** — hot, latest, most popular, completed, and dozens of genres per source
|
|
30
|
+
- **My Library** — browse locally downloaded novels, resume reading, delete
|
|
31
|
+
- **Reader** — next/prev (`n`/`p`), jump to chapter (`j`), download current (`d`)
|
|
32
|
+
- **Progress tracking** — auto-saves last chapter; ✓ marks read chapters
|
|
33
|
+
- **Download dialog** — All, Range, or Translated; progress bar with translation warning
|
|
34
|
+
- **Translation** — Google Translate (12 languages); Arabic RTL layout with shaped text
|
|
35
|
+
- **EPUB export** — generate EPUB files from downloaded chapters
|
|
36
|
+
- **Delete with safety** — double-press `x` to confirm deletion
|
|
37
|
+
|
|
38
|
+
Key bindings in the reader:
|
|
39
|
+
|
|
40
|
+
| Key | Action |
|
|
41
|
+
| ----- | -------- |
|
|
42
|
+
| `n` / `p` | Next / Prev chapter |
|
|
43
|
+
| `j` | Jump to chapter number |
|
|
44
|
+
| `d` | Download dialog (All / Range / Translated) |
|
|
45
|
+
| `t` | Translate to language |
|
|
46
|
+
| `r` | Revert to original text |
|
|
47
|
+
| `h` | Go to main menu |
|
|
48
|
+
| `q` | Back to chapter list |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Two frontends, one core
|
|
53
|
+
|
|
54
|
+
- `main.py` — entry-point dispatcher: TUI on desktop, GUI on Android (`ANDROID_ARGUMENT`).
|
|
55
|
+
- `tui/` — **Textual TUI**: `main.py` (app), `main_menu.py`, `browse.py`, `reader.py`, `library.py`, `download.py`, `source_picker.py`, `shared.py`, `utils.py`, `novelfetch.tcss`.
|
|
56
|
+
- `gui/` — **KivyMD GUI** (desktop + Android): `main.py`, `screens/` (`main_screen`, `home_tab`, `search_tab`, `update`, `history`, `settings_tab`, `novel_list`, `chapter_list`, `reader`, `download_picker`, `download_dialog`, `source_picker`, `topbar`, `theme`, `app_settings`), `kv/` layout files, `data/` (icon, bundled Arabic font).
|
|
57
|
+
- `core/` — shared framework: `progress.py`, `translation.py`, `epub.py`, `downloader.py`, `http_client.py`, `library.py`, `utils.py` (source dispatch), `paths.py` (data-dir resolution for frozen/AppImage/Android).
|
|
58
|
+
- `sources/` — pluggable `Source` interface (`base.py`) and a `REGISTRY` of scrapers.
|
|
59
|
+
- `novels/` — downloaded chapters, per-novel `meta.json` / `cover.*`, `progress.json`, `tracking.json`.
|
|
60
|
+
|
|
61
|
+
## Pluggable sources
|
|
62
|
+
|
|
63
|
+
The `Source` abstraction (`sources/base.py:1`) defines browsing, search, chapter fetching, and cover resolution; implementations are registered in `sources/__init__.py:7`:
|
|
64
|
+
|
|
65
|
+
| Source | Browse lists | Genres | Notes |
|
|
66
|
+
|--------|-------------|--------|-------|
|
|
67
|
+
| **NovelFire** | 9 | 47 | Throttled paging (`PAGE_DELAY = 0.4`) — novelfire.net returns 429 on burst paging |
|
|
68
|
+
| **NovelPhoenix** | 9 | 55 | Condensed 79-column ASCII banner so it doesn't wrap |
|
|
69
|
+
| **RoyalRoad** | 8 (hot, popular, latest, newest, completed, rising stars, ongoing, more) | 13 | AJAX-free chapter table |
|
|
70
|
+
| **ScribbleHub** | 4 (hot, latest, popular, completed) | 25 | Cloudflare-aware; lazy `curl_cffi` with httpx fallback (p4a-safe) |
|
|
71
|
+
| **WuxiaSpot** | 7 | 48 | AJAX pagination (`fy.php`); search marked unreliable |
|
|
72
|
+
|
|
73
|
+
New sources implement the same interface and register a key — no frontend changes needed.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
### From PyPI
|
|
80
|
+
|
|
81
|
+
Requires **Python 3.10+**.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install novelfetch # terminal app (TUI)
|
|
85
|
+
novelfetch-tui
|
|
86
|
+
|
|
87
|
+
pip install "novelfetch[gui]" # + KivyMD desktop GUI
|
|
88
|
+
novelfetch-gui
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The TUI is the primary frontend, so its dependencies (`textual`, `curl_cffi`) are
|
|
92
|
+
installed by default and `pip install novelfetch` works out of the box. The KivyMD
|
|
93
|
+
GUI is opt-in because it pulls in Kivy — use the `[gui]` extra for the desktop
|
|
94
|
+
frontend, or the APK for Android.
|
|
95
|
+
|
|
96
|
+
Note that installing puts the top-level modules `core`, `tui`, `gui`, and `sources`
|
|
97
|
+
in `site-packages`. They are generic names and can collide with unrelated packages;
|
|
98
|
+
if you hit an import conflict, install into a dedicated virtualenv.
|
|
99
|
+
|
|
100
|
+
User data (novels, reading progress, settings) lives in `~/.novelfetch`, or
|
|
101
|
+
`$NOVELFETCH_DATA_DIR` if you set it.
|
|
102
|
+
|
|
103
|
+
### From source
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/Momokh99/NovelFetch.git
|
|
107
|
+
cd NovelFetch
|
|
108
|
+
make setup # creates both environments (TUI + GUI)
|
|
109
|
+
make pre-commit-install
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Running from source
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
make run-tui # Textual TUI (terminal)
|
|
116
|
+
make run-kivy # KivyMD GUI (desktop)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### What `make setup` does
|
|
120
|
+
|
|
121
|
+
The project uses two separate virtual environments:
|
|
122
|
+
|
|
123
|
+
| Environment | Purpose | Created by |
|
|
124
|
+
|-------------|---------|------------|
|
|
125
|
+
| `myenv/` | TUI + code quality tools (Ruff, mypy, Pyright) | `make setup-tui` |
|
|
126
|
+
| `android_env/` | KivyMD GUI (desktop + Android) | `make setup-android` |
|
|
127
|
+
|
|
128
|
+
Dependencies are split across `pyproject.toml` optional groups:
|
|
129
|
+
- **Core** (always installed): `httpx`, `beautifulsoup4`, `lxml`, `deep-translator`, `ebooklib`, `textual`, `curl_cffi`
|
|
130
|
+
- **GUI** (`pip install "novelfetch[gui]"`): `kivy`, `kivymd`, `arabic-reshaper`, `python-bidi`
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## How It Works
|
|
135
|
+
|
|
136
|
+
**Architecture:**
|
|
137
|
+
|
|
138
|
+
The package layout is described once, in [Two frontends, one core](#two-frontends-one-core) above — `main.py` dispatches to either `tui/` or `gui/`, both of which sit on `core/` and `sources/`.
|
|
139
|
+
|
|
140
|
+
The GUI is the richer frontend: a 5-tab `MDNavigationBar` (Home / Search / Updates / History / Settings) plus per-novel browse flow. Settings persist to `app_settings.json`; update checks reconcile against `update_results.json`. Downloads run concurrently (`core/downloader.py` caps at 4) with a live progress bar.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Android packaging
|
|
145
|
+
|
|
146
|
+
`buildozer.spec` builds a debug APK with python-for-android:
|
|
147
|
+
|
|
148
|
+
- arm64-v8a, API 34 (min 21), NDK 25b
|
|
149
|
+
- p4a pinned to `v2026.05.09` (avoids nightly-master breakage with pip ≥ 26)
|
|
150
|
+
- CI (`release.yml`) builds and publishes the APK with every tagged release
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
make android-debug # build debug APK
|
|
154
|
+
make android-deploy # deploy to connected device
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Roadmap
|
|
160
|
+
|
|
161
|
+
- [x] TUI mode (Textual interface)
|
|
162
|
+
- [x] GUI mode (KivyMD desktop + Android)
|
|
163
|
+
- [x] Resume from last chapter (progress.json)
|
|
164
|
+
- [x] Search with auto-type and pagination
|
|
165
|
+
- [x] Translation (Google Translate, 12 languages, RTL support)
|
|
166
|
+
- [x] Multi-source architecture (NovelFire, NovelPhoenix, RoyalRoad, ScribbleHub, WuxiaSpot)
|
|
167
|
+
- [x] Download dialog (All, Range, Translated)
|
|
168
|
+
- [x] Offline reading mode
|
|
169
|
+
- [x] Reading history across sessions
|
|
170
|
+
- [x] EPUB export
|
|
171
|
+
- [x] APK build via Buildozer
|
|
172
|
+
- [x] PyPI distribution (TUI by default, GUI via the `[gui]` extra)
|
|
173
|
+
- [ ] Better text formatting (italics, line breaks, spacing)
|
|
174
|
+
- [ ] Search filters (genre, status, rating)
|
|
175
|
+
- [ ] Adding to AUR — blocked: AUR account registration disabled upstream; packages ready to push once sign-ups reopen
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
### Quick Start
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# One-time setup
|
|
185
|
+
make setup
|
|
186
|
+
make pre-commit-install
|
|
187
|
+
|
|
188
|
+
# Daily development
|
|
189
|
+
make run-tui # Run the TUI app
|
|
190
|
+
make run-kivy # Run the KivyMD GUI app
|
|
191
|
+
make lint # Check code quality
|
|
192
|
+
make format # Auto-format code
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Available Commands
|
|
196
|
+
|
|
197
|
+
| Command | Description |
|
|
198
|
+
|---------|-------------|
|
|
199
|
+
| `make setup` | Create both venvs and install all dependencies |
|
|
200
|
+
| `make run-tui` | Run TUI app (myenv) |
|
|
201
|
+
| `make run-kivy` | Run KivyMD GUI app (android_env) |
|
|
202
|
+
| `make lint` | Run Ruff + mypy + Pyright |
|
|
203
|
+
| `make lint-fix` | Auto-fix lint issues |
|
|
204
|
+
| `make format` | Auto-format code (ruff format + import sorting) |
|
|
205
|
+
| `make format-check` | Check formatting without modifying |
|
|
206
|
+
| `make android-debug` | Build debug APK |
|
|
207
|
+
| `make android-release` | Build release APK |
|
|
208
|
+
| `make android-deploy` | Deploy to connected device |
|
|
209
|
+
| `make android-logcat` | Show device logcat |
|
|
210
|
+
| `make android-clean` | Clean buildozer build artifacts |
|
|
211
|
+
| `make pre-commit-run` | Run pre-commit on all files |
|
|
212
|
+
| `make clean` | Remove build artifacts and caches |
|
|
213
|
+
| `make bump-release VERSION=x.y.z` | Bump version in pyproject.toml + buildozer.spec |
|
|
214
|
+
| `make pkg` | Build the wheel + sdist into `dist/` and check their metadata |
|
|
215
|
+
| `make pkg-publish` | Upload `dist/*` to PyPI (manual fallback for the publish workflow) |
|
|
216
|
+
|
|
217
|
+
### Development Tools
|
|
218
|
+
|
|
219
|
+
- **Linting**: Ruff locally via `make lint`; CI keeps a flake8 syntax/undefined-name gate
|
|
220
|
+
- **Type Checking**: Mypy + Pyright
|
|
221
|
+
- **Pre-commit**: Auto-format on commit
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Disclaimer
|
|
226
|
+
|
|
227
|
+
This tool scrapes publicly available content for personal use only. Novels belong to their authors and translators. Do not redistribute downloaded content. Support the creators if you enjoy their work.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Download orchestrator — shared by both GUI and TUI.
|
|
2
|
+
|
|
3
|
+
This module replaces the duplicated download logic that previously lived in
|
|
4
|
+
:mod:`gui.screens.utils` (``_download_novel``) and :mod:`tui.download`
|
|
5
|
+
(``DownloadProgressScreen._download_all``). It owns the concurrent
|
|
6
|
+
semaphore, chapter‑by‑chapter orchestration, progress reporting, and the
|
|
7
|
+
final metadata/cover write‑back.
|
|
8
|
+
|
|
9
|
+
The public API is a single async function:
|
|
10
|
+
|
|
11
|
+
await download(source, qualified_slug, chapters, title, ...)
|
|
12
|
+
|
|
13
|
+
which returns ``(saved, failed)`` — exactly matching the signatures that
|
|
14
|
+
callers currently expect.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
from core.library import (
|
|
21
|
+
read_meta,
|
|
22
|
+
write_chapter,
|
|
23
|
+
write_meta,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DOWNLOAD_CONCURRENCY: int = 4
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def download(
|
|
30
|
+
source,
|
|
31
|
+
qualified_slug,
|
|
32
|
+
chapters,
|
|
33
|
+
title,
|
|
34
|
+
*,
|
|
35
|
+
translate=False,
|
|
36
|
+
lang="",
|
|
37
|
+
total=None,
|
|
38
|
+
progress_cb=None,
|
|
39
|
+
base_dir=None,
|
|
40
|
+
) -> tuple[int, int]:
|
|
41
|
+
"""Download every chapter of a novel and persist the results.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
source : Source
|
|
46
|
+
The scraper source providing ``read_chapter`` (and ``cover_url`` if
|
|
47
|
+
a cover is needed).
|
|
48
|
+
qualified_slug : str
|
|
49
|
+
The source-qualified slug, e.g. ``royalroad:some-title``.
|
|
50
|
+
chapters : list[dict]
|
|
51
|
+
Chapter entries as returned by :func:`core.utils._get_chapters`.
|
|
52
|
+
title : str
|
|
53
|
+
Novel title (written into meta.json).
|
|
54
|
+
translate : bool
|
|
55
|
+
When True, each chapter is fetched via :func:`source.read_chapter`,
|
|
56
|
+
then translated with :func:`core.translation._translate_text`.
|
|
57
|
+
lang : str
|
|
58
|
+
Translation language code (e.g. ``"ar"``).
|
|
59
|
+
total : int, optional
|
|
60
|
+
Total chapter count for meta.json. Defaults to ``len(chapters)``.
|
|
61
|
+
progress_cb : callable, optional
|
|
62
|
+
``progress_cb(done, saved)`` is called after each chapter completes.
|
|
63
|
+
base_dir : str, optional
|
|
64
|
+
Root of the novels directory. Defaults to the process working
|
|
65
|
+
directory (matching historic ``CWD``-relative behaviour).
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
(saved, failed) : tuple[int, int]
|
|
70
|
+
*saved* counts chapters that were successfully fetched/saved;
|
|
71
|
+
*failed* counts chapters that could not be fetched.
|
|
72
|
+
"""
|
|
73
|
+
novels_dir = os.path.join(base_dir or os.getcwd(), "novels")
|
|
74
|
+
if total is None:
|
|
75
|
+
total = len(chapters)
|
|
76
|
+
saved = 0
|
|
77
|
+
failed = 0
|
|
78
|
+
done = 0
|
|
79
|
+
|
|
80
|
+
sem = asyncio.Semaphore(DOWNLOAD_CONCURRENCY)
|
|
81
|
+
|
|
82
|
+
async def _process(ch):
|
|
83
|
+
nonlocal saved, failed, done
|
|
84
|
+
safe_title = ch["title"].replace("/", "-").replace(" ", "_")
|
|
85
|
+
# Determine the file path for this chapter
|
|
86
|
+
if translate:
|
|
87
|
+
path = os.path.join(novels_dir, qualified_slug, f"{safe_title}_{lang}.txt")
|
|
88
|
+
else:
|
|
89
|
+
path = os.path.join(novels_dir, qualified_slug, f"{safe_title}.txt")
|
|
90
|
+
|
|
91
|
+
# Skip if the chapter file already exists (preserves "already
|
|
92
|
+
# downloaded" behaviour from the former GUI _download_novel).
|
|
93
|
+
if os.path.exists(path):
|
|
94
|
+
done += 1
|
|
95
|
+
if progress_cb is not None:
|
|
96
|
+
progress_cb(done, saved)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
async with sem:
|
|
100
|
+
try:
|
|
101
|
+
if translate:
|
|
102
|
+
lines = await source.read_chapter(ch["url"])
|
|
103
|
+
if not lines:
|
|
104
|
+
failed += 1
|
|
105
|
+
else:
|
|
106
|
+
text = "\n\n".join(lines)
|
|
107
|
+
# Translate using the shared core.translation helper.
|
|
108
|
+
# Import inside the function to avoid triggering the
|
|
109
|
+
# module-level deep_translator dependency.
|
|
110
|
+
try:
|
|
111
|
+
from core.translation import _translate_text as _tt
|
|
112
|
+
|
|
113
|
+
translated = _tt(text, target=lang)
|
|
114
|
+
except ImportError:
|
|
115
|
+
translated = text
|
|
116
|
+
if not translated:
|
|
117
|
+
failed += 1
|
|
118
|
+
else:
|
|
119
|
+
write_chapter(
|
|
120
|
+
qualified_slug,
|
|
121
|
+
ch["title"],
|
|
122
|
+
translated,
|
|
123
|
+
lang=lang,
|
|
124
|
+
base_dir=base_dir,
|
|
125
|
+
)
|
|
126
|
+
saved += 1
|
|
127
|
+
else:
|
|
128
|
+
lines = await source.read_chapter(ch["url"])
|
|
129
|
+
if not lines:
|
|
130
|
+
failed += 1
|
|
131
|
+
else:
|
|
132
|
+
text = "\n\n".join(lines)
|
|
133
|
+
write_chapter(
|
|
134
|
+
qualified_slug,
|
|
135
|
+
ch["title"],
|
|
136
|
+
text,
|
|
137
|
+
lang=None,
|
|
138
|
+
base_dir=base_dir,
|
|
139
|
+
)
|
|
140
|
+
saved += 1
|
|
141
|
+
except Exception:
|
|
142
|
+
failed += 1
|
|
143
|
+
done += 1
|
|
144
|
+
if progress_cb is not None:
|
|
145
|
+
progress_cb(done, saved)
|
|
146
|
+
|
|
147
|
+
await asyncio.gather(*(_process(ch) for ch in chapters))
|
|
148
|
+
|
|
149
|
+
# ---- completion: write meta.json + cover ----
|
|
150
|
+
# Build meta
|
|
151
|
+
try:
|
|
152
|
+
cover = await source.cover_url(
|
|
153
|
+
qualified_slug.split(":", 1)[-1]
|
|
154
|
+
if ":" in qualified_slug
|
|
155
|
+
else qualified_slug
|
|
156
|
+
)
|
|
157
|
+
except Exception:
|
|
158
|
+
cover = ""
|
|
159
|
+
meta = {"title": title, "cover": cover}
|
|
160
|
+
if translate and lang:
|
|
161
|
+
meta["lang"] = lang
|
|
162
|
+
meta["chapters"] = total
|
|
163
|
+
# Merge with existing meta (preserves 'tracked' from prior tracking)
|
|
164
|
+
existing_meta = read_meta(qualified_slug, base_dir)
|
|
165
|
+
existing_meta.update(meta)
|
|
166
|
+
write_meta(qualified_slug, existing_meta, base_dir)
|
|
167
|
+
|
|
168
|
+
return saved, failed
|