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.
Files changed (73) hide show
  1. novelfetch-2.0.9/LICENSE +21 -0
  2. novelfetch-2.0.9/PKG-INFO +256 -0
  3. novelfetch-2.0.9/README.md +227 -0
  4. novelfetch-2.0.9/core/__init__.py +5 -0
  5. novelfetch-2.0.9/core/downloader.py +168 -0
  6. novelfetch-2.0.9/core/epub.py +143 -0
  7. novelfetch-2.0.9/core/http_client.py +262 -0
  8. novelfetch-2.0.9/core/library.py +423 -0
  9. novelfetch-2.0.9/core/paths.py +69 -0
  10. novelfetch-2.0.9/core/progress.py +201 -0
  11. novelfetch-2.0.9/core/translation.py +93 -0
  12. novelfetch-2.0.9/core/utils.py +83 -0
  13. novelfetch-2.0.9/gui/__init__.py +1 -0
  14. novelfetch-2.0.9/gui/async_runner.py +100 -0
  15. novelfetch-2.0.9/gui/data/NotoNaskhArabic.ttf +0 -0
  16. novelfetch-2.0.9/gui/data/icon.png +0 -0
  17. novelfetch-2.0.9/gui/kv/chapter_list.kv +86 -0
  18. novelfetch-2.0.9/gui/kv/download_dialog.kv +47 -0
  19. novelfetch-2.0.9/gui/kv/download_picker.kv +62 -0
  20. novelfetch-2.0.9/gui/kv/history.kv +50 -0
  21. novelfetch-2.0.9/gui/kv/home_tab.kv +208 -0
  22. novelfetch-2.0.9/gui/kv/novel_list.kv +44 -0
  23. novelfetch-2.0.9/gui/kv/reader.kv +71 -0
  24. novelfetch-2.0.9/gui/kv/search_tab.kv +121 -0
  25. novelfetch-2.0.9/gui/kv/settings_tab.kv +126 -0
  26. novelfetch-2.0.9/gui/kv/topbar.kv +24 -0
  27. novelfetch-2.0.9/gui/kv/update.kv +60 -0
  28. novelfetch-2.0.9/gui/main.py +154 -0
  29. novelfetch-2.0.9/gui/novelfetch.kv +11 -0
  30. novelfetch-2.0.9/gui/screens/__init__.py +1 -0
  31. novelfetch-2.0.9/gui/screens/app_settings.py +82 -0
  32. novelfetch-2.0.9/gui/screens/browse.py +162 -0
  33. novelfetch-2.0.9/gui/screens/chapter_list.py +540 -0
  34. novelfetch-2.0.9/gui/screens/download_dialog.py +133 -0
  35. novelfetch-2.0.9/gui/screens/download_picker.py +195 -0
  36. novelfetch-2.0.9/gui/screens/history.py +212 -0
  37. novelfetch-2.0.9/gui/screens/home_tab.py +1593 -0
  38. novelfetch-2.0.9/gui/screens/main_screen.py +134 -0
  39. novelfetch-2.0.9/gui/screens/novel_list.py +43 -0
  40. novelfetch-2.0.9/gui/screens/reader.py +660 -0
  41. novelfetch-2.0.9/gui/screens/search_tab.py +278 -0
  42. novelfetch-2.0.9/gui/screens/settings_tab.py +345 -0
  43. novelfetch-2.0.9/gui/screens/source_picker.py +73 -0
  44. novelfetch-2.0.9/gui/screens/theme.py +63 -0
  45. novelfetch-2.0.9/gui/screens/topbar.py +72 -0
  46. novelfetch-2.0.9/gui/screens/update.py +490 -0
  47. novelfetch-2.0.9/gui/screens/utils.py +811 -0
  48. novelfetch-2.0.9/novelfetch.egg-info/PKG-INFO +256 -0
  49. novelfetch-2.0.9/novelfetch.egg-info/SOURCES.txt +71 -0
  50. novelfetch-2.0.9/novelfetch.egg-info/dependency_links.txt +1 -0
  51. novelfetch-2.0.9/novelfetch.egg-info/entry_points.txt +3 -0
  52. novelfetch-2.0.9/novelfetch.egg-info/requires.txt +19 -0
  53. novelfetch-2.0.9/novelfetch.egg-info/top_level.txt +4 -0
  54. novelfetch-2.0.9/pyproject.toml +110 -0
  55. novelfetch-2.0.9/setup.cfg +4 -0
  56. novelfetch-2.0.9/sources/__init__.py +14 -0
  57. novelfetch-2.0.9/sources/base.py +93 -0
  58. novelfetch-2.0.9/sources/novelfire.py +266 -0
  59. novelfetch-2.0.9/sources/novelphoenix.py +295 -0
  60. novelfetch-2.0.9/sources/royalroad.py +158 -0
  61. novelfetch-2.0.9/sources/scriblehub.py +264 -0
  62. novelfetch-2.0.9/sources/wuxiaspot.py +328 -0
  63. novelfetch-2.0.9/tui/__init__.py +12 -0
  64. novelfetch-2.0.9/tui/browse.py +340 -0
  65. novelfetch-2.0.9/tui/download.py +447 -0
  66. novelfetch-2.0.9/tui/library.py +223 -0
  67. novelfetch-2.0.9/tui/main.py +46 -0
  68. novelfetch-2.0.9/tui/main_menu.py +123 -0
  69. novelfetch-2.0.9/tui/novelfetch.tcss +128 -0
  70. novelfetch-2.0.9/tui/reader.py +264 -0
  71. novelfetch-2.0.9/tui/shared.py +112 -0
  72. novelfetch-2.0.9/tui/source_picker.py +37 -0
  73. novelfetch-2.0.9/tui/utils.py +3 -0
@@ -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
+ ![menu](https://img.shields.io/badge/built%20with-Textual-blue)
50
+ ![python](https://img.shields.io/badge/python-3.9%2B-blue)
51
+ ![license](https://img.shields.io/badge/license-MIT-green)
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
+ ![menu](https://img.shields.io/badge/built%20with-Textual-blue)
21
+ ![python](https://img.shields.io/badge/python-3.9%2B-blue)
22
+ ![license](https://img.shields.io/badge/license-MIT-green)
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,5 @@
1
+ """Core shared framework used by both the TUI and GUI frontends.
2
+
3
+ Hosts logic that is independent of any particular UI: progress tracking,
4
+ translation, EPUB export, and source dispatch helpers.
5
+ """
@@ -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