weeb-cli 2.13.2__tar.gz → 2.14.2__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.
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/PKG-INFO +38 -5
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/README.md +32 -2
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/pyproject.toml +4 -3
- weeb_cli-2.14.2/tests/test_restful_api.py +227 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/__init__.py +1 -1
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/watch_flow.py +38 -24
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/serve.py +34 -4
- weeb_cli-2.14.2/weeb_cli/commands/serve_restful.py +384 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/en/hianime.py +1 -1
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/registry.py +17 -5
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/PKG-INFO +38 -5
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/SOURCES.txt +2 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/requires.txt +4 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/LICENSE +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/setup.cfg +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_anilist_tracker.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_aniworld_provider.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_api.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_cache.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_exceptions.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_kitsu_tracker.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_mal_tracker.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_providers.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_sanitizer.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_sanitizer_security.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/__main__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/api.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/downloads.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/library.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/anime_details.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/download_flow.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/episode_utils.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/search_handlers.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/stream_utils.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_backup.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_cache.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_config.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_download.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_drives.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_menu.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_shortcuts.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_trackers.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/setup.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/watchlist.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/config.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/exceptions.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/i18n.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/locales/de.json +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/locales/en.json +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/locales/pl.json +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/locales/tr.json +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/main.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/base.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/de/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/de/aniworld.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/en/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/en/allanime.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/doodstream.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/filemoon.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/megacloud.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/streamtape.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/vidoza.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/voe.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/pl/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/pl/docchi.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/tr/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/tr/animecix.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/tr/anizle.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/tr/turkanime.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/tr/weeb.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/_base.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/_tracker_base.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/aniskip.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/cache.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/database.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/dependency_manager.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/details.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/discord_rpc.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/downloader.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/error_handler.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/headless_downloader.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/local_library.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/logger.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/notifier.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/player.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/progress.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/scraper.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/search.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/shortcuts.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/stream_validator.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/tracker.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/updater.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/watch.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/templates/anilist_error.html +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/templates/anilist_success.html +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/templates/mal_error.html +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/templates/mal_success.html +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/ui/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/ui/header.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/ui/menu.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/ui/prompt.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/utils/__init__.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/utils/sanitizer.py +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/dependency_links.txt +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/entry_points.txt +0 -0
- {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weeb-cli
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.14.2
|
|
4
|
+
Summary: No browser, no ads, no distractions. Just you and an unparalleled anime viewing experience.
|
|
5
5
|
Author-email: ewgsta <ewgst@proton.me>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
7
7
|
Project-URL: Homepage, https://weeb-cli.ewgsta.me
|
|
8
8
|
Project-URL: Repository, https://github.com/ewgsta/weeb-cli
|
|
9
9
|
Project-URL: Issues, https://github.com/ewgsta/weeb-cli/issues
|
|
10
|
-
Keywords: anime,
|
|
10
|
+
Keywords: terminal,anime,tui,crossplatform,anime-download,turkanime,anizm,anime-watch,aniwatch,anime-cli,ani-cli,openanime,allanime,hianime,anitr-cli,animecix,anime-indir,anime-izle,weeb-cli,katsuanime
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Classifier: Environment :: Console
|
|
@@ -32,6 +32,9 @@ Requires-Dist: winotify; sys_platform == "win32"
|
|
|
32
32
|
Provides-Extra: serve
|
|
33
33
|
Requires-Dist: flask>=3.0; extra == "serve"
|
|
34
34
|
Requires-Dist: pyyaml>=6.0; extra == "serve"
|
|
35
|
+
Provides-Extra: serve-restful
|
|
36
|
+
Requires-Dist: flask>=3.0; extra == "serve-restful"
|
|
37
|
+
Requires-Dist: flask-cors>=4.0; extra == "serve-restful"
|
|
35
38
|
Provides-Extra: dev
|
|
36
39
|
Requires-Dist: build; extra == "dev"
|
|
37
40
|
Requires-Dist: pyinstaller; extra == "dev"
|
|
@@ -52,7 +55,7 @@ Dynamic: license-file
|
|
|
52
55
|
</p>
|
|
53
56
|
|
|
54
57
|
<div align="center">
|
|
55
|
-
<a href="README.md">English</a> | <a href="md/tr/README.md">Türkçe</a> | <a href="md/de/README.md">Deutsch</a> | <a href="md/pl/README.md">Polski</a>
|
|
58
|
+
<a href="README.md">English</a> | <a href="docs/md/tr/README.md">Türkçe</a> | <a href="docs/md/de/README.md">Deutsch</a> | <a href="docs/md/pl/README.md">Polski</a>
|
|
56
59
|
</div>
|
|
57
60
|
<br>
|
|
58
61
|
|
|
@@ -118,6 +121,7 @@ Dynamic: license-file
|
|
|
118
121
|
- Automatic update checks
|
|
119
122
|
- Non-interactive JSON API for scripts and AI agents
|
|
120
123
|
- Torznab server mode for Sonarr/*arr integration
|
|
124
|
+
- RESTful API server for web/mobile applications
|
|
121
125
|
|
|
122
126
|
---
|
|
123
127
|
|
|
@@ -195,7 +199,34 @@ weeb-cli serve --port 9876 \
|
|
|
195
199
|
|
|
196
200
|
Then add `http://weeb-cli-host:9876` as a Torznab indexer in Sonarr with category 5070 (TV/Anime). The server includes a blackhole download worker that automatically processes grabbed episodes.
|
|
197
201
|
|
|
198
|
-
|
|
202
|
+
### RESTful API Server
|
|
203
|
+
|
|
204
|
+
For web/mobile applications and custom integrations, weeb-cli provides a RESTful API server:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
pip install weeb-cli[serve-restful]
|
|
208
|
+
|
|
209
|
+
weeb-cli serve restful --port 8080 --cors
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**API Endpoints:**
|
|
213
|
+
- `GET /health` - Health check
|
|
214
|
+
- `GET /api/providers` - List available providers
|
|
215
|
+
- `GET /api/search?q=naruto&provider=animecix` - Search anime
|
|
216
|
+
- `GET /api/anime/{id}?provider=animecix` - Get anime details
|
|
217
|
+
- `GET /api/anime/{id}/episodes?season=1` - List episodes
|
|
218
|
+
- `GET /api/anime/{id}/episodes/{ep_id}/streams` - Get stream URLs
|
|
219
|
+
|
|
220
|
+
All available providers are loaded automatically. Select which provider to use via the `provider` query parameter.
|
|
221
|
+
|
|
222
|
+
**Docker Support:**
|
|
223
|
+
```bash
|
|
224
|
+
docker-compose -f docs/docker-compose.restful.yml up -d
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
See [RESTful API Documentation](https://ewgsta.github.io/weeb-cli/cli/restful-api/) for full details.
|
|
228
|
+
|
|
229
|
+
#### Docker (Torznab)
|
|
199
230
|
|
|
200
231
|
```dockerfile
|
|
201
232
|
FROM python:3.13-slim
|
|
@@ -292,6 +323,7 @@ All settings can be modified through the interactive Settings menu.
|
|
|
292
323
|
- [x] Keyboard shortcuts
|
|
293
324
|
- [x] Non-interactive API mode (JSON output)
|
|
294
325
|
- [x] Torznab server for Sonarr/*arr integration
|
|
326
|
+
- [x] RESTful API server for web/mobile apps
|
|
295
327
|
|
|
296
328
|
|
|
297
329
|
### Planned
|
|
@@ -315,6 +347,7 @@ weeb-cli/
|
|
|
315
347
|
│ │ ├── downloads.py # Download management commands
|
|
316
348
|
│ │ ├── search.py # Anime search functionality
|
|
317
349
|
│ │ ├── serve.py # Torznab server for *arr integration
|
|
350
|
+
│ │ ├── serve_restful.py # RESTful API server
|
|
318
351
|
│ │ ├── settings.py # Settings menu and configuration
|
|
319
352
|
│ │ ├── setup.py # Initial setup wizard
|
|
320
353
|
│ │ └── watchlist.py # Watch history and progress
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<div align="center">
|
|
12
|
-
<a href="README.md">English</a> | <a href="md/tr/README.md">Türkçe</a> | <a href="md/de/README.md">Deutsch</a> | <a href="md/pl/README.md">Polski</a>
|
|
12
|
+
<a href="README.md">English</a> | <a href="docs/md/tr/README.md">Türkçe</a> | <a href="docs/md/de/README.md">Deutsch</a> | <a href="docs/md/pl/README.md">Polski</a>
|
|
13
13
|
</div>
|
|
14
14
|
<br>
|
|
15
15
|
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
- Automatic update checks
|
|
76
76
|
- Non-interactive JSON API for scripts and AI agents
|
|
77
77
|
- Torznab server mode for Sonarr/*arr integration
|
|
78
|
+
- RESTful API server for web/mobile applications
|
|
78
79
|
|
|
79
80
|
---
|
|
80
81
|
|
|
@@ -152,7 +153,34 @@ weeb-cli serve --port 9876 \
|
|
|
152
153
|
|
|
153
154
|
Then add `http://weeb-cli-host:9876` as a Torznab indexer in Sonarr with category 5070 (TV/Anime). The server includes a blackhole download worker that automatically processes grabbed episodes.
|
|
154
155
|
|
|
155
|
-
|
|
156
|
+
### RESTful API Server
|
|
157
|
+
|
|
158
|
+
For web/mobile applications and custom integrations, weeb-cli provides a RESTful API server:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pip install weeb-cli[serve-restful]
|
|
162
|
+
|
|
163
|
+
weeb-cli serve restful --port 8080 --cors
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**API Endpoints:**
|
|
167
|
+
- `GET /health` - Health check
|
|
168
|
+
- `GET /api/providers` - List available providers
|
|
169
|
+
- `GET /api/search?q=naruto&provider=animecix` - Search anime
|
|
170
|
+
- `GET /api/anime/{id}?provider=animecix` - Get anime details
|
|
171
|
+
- `GET /api/anime/{id}/episodes?season=1` - List episodes
|
|
172
|
+
- `GET /api/anime/{id}/episodes/{ep_id}/streams` - Get stream URLs
|
|
173
|
+
|
|
174
|
+
All available providers are loaded automatically. Select which provider to use via the `provider` query parameter.
|
|
175
|
+
|
|
176
|
+
**Docker Support:**
|
|
177
|
+
```bash
|
|
178
|
+
docker-compose -f docs/docker-compose.restful.yml up -d
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
See [RESTful API Documentation](https://ewgsta.github.io/weeb-cli/cli/restful-api/) for full details.
|
|
182
|
+
|
|
183
|
+
#### Docker (Torznab)
|
|
156
184
|
|
|
157
185
|
```dockerfile
|
|
158
186
|
FROM python:3.13-slim
|
|
@@ -249,6 +277,7 @@ All settings can be modified through the interactive Settings menu.
|
|
|
249
277
|
- [x] Keyboard shortcuts
|
|
250
278
|
- [x] Non-interactive API mode (JSON output)
|
|
251
279
|
- [x] Torznab server for Sonarr/*arr integration
|
|
280
|
+
- [x] RESTful API server for web/mobile apps
|
|
252
281
|
|
|
253
282
|
|
|
254
283
|
### Planned
|
|
@@ -272,6 +301,7 @@ weeb-cli/
|
|
|
272
301
|
│ │ ├── downloads.py # Download management commands
|
|
273
302
|
│ │ ├── search.py # Anime search functionality
|
|
274
303
|
│ │ ├── serve.py # Torznab server for *arr integration
|
|
304
|
+
│ │ ├── serve_restful.py # RESTful API server
|
|
275
305
|
│ │ ├── settings.py # Settings menu and configuration
|
|
276
306
|
│ │ ├── setup.py # Initial setup wizard
|
|
277
307
|
│ │ └── watchlist.py # Watch history and progress
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weeb-cli"
|
|
7
|
-
version = "2.
|
|
8
|
-
description = "
|
|
7
|
+
version = "2.14.2"
|
|
8
|
+
description = "No browser, no ads, no distractions. Just you and an unparalleled anime viewing experience."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "ewgsta", email = "ewgst@proton.me" }]
|
|
11
11
|
license = "GPL-3.0-only"
|
|
@@ -14,7 +14,7 @@ classifiers = [
|
|
|
14
14
|
"Operating System :: OS Independent",
|
|
15
15
|
"Environment :: Console",
|
|
16
16
|
]
|
|
17
|
-
keywords = ["anime", "
|
|
17
|
+
keywords = ["terminal", "anime", "tui", "crossplatform", "anime-download", "turkanime", "anizm", "anime-watch", "aniwatch", "anime-cli", "ani-cli", "openanime", "allanime", "hianime", "anitr-cli", "animecix", "anime-indir", "anime-izle", "weeb-cli", "katsuanime"]
|
|
18
18
|
requires-python = ">=3.12"
|
|
19
19
|
dependencies = [
|
|
20
20
|
"typer[all]",
|
|
@@ -36,6 +36,7 @@ dependencies = [
|
|
|
36
36
|
|
|
37
37
|
[project.optional-dependencies]
|
|
38
38
|
serve = ["flask>=3.0", "pyyaml>=6.0"]
|
|
39
|
+
serve-restful = ["flask>=3.0", "flask-cors>=4.0"]
|
|
39
40
|
dev = ["build", "pyinstaller", "pytest>=7.0.0", "pytest-cov", "pytest-asyncio", "pytest-mock"]
|
|
40
41
|
|
|
41
42
|
[tool.setuptools.packages.find]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Tests for RESTful API server."""
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
4
|
+
from weeb_cli.providers.base import AnimeResult, Episode, StreamLink
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
def mock_provider():
|
|
9
|
+
"""Create a mock provider."""
|
|
10
|
+
provider = Mock()
|
|
11
|
+
provider.name = "test_provider"
|
|
12
|
+
provider.search.return_value = [
|
|
13
|
+
AnimeResult(
|
|
14
|
+
id="test123",
|
|
15
|
+
title="Test Anime",
|
|
16
|
+
type="series",
|
|
17
|
+
cover="https://example.com/cover.jpg",
|
|
18
|
+
year=2024,
|
|
19
|
+
)
|
|
20
|
+
]
|
|
21
|
+
provider.get_episodes.return_value = [
|
|
22
|
+
Episode(id="ep1", number=1, title="Episode 1", season=1),
|
|
23
|
+
Episode(id="ep2", number=2, title="Episode 2", season=1),
|
|
24
|
+
]
|
|
25
|
+
provider.get_streams.return_value = [
|
|
26
|
+
StreamLink(
|
|
27
|
+
url="https://example.com/stream.m3u8",
|
|
28
|
+
quality="1080p",
|
|
29
|
+
server="default",
|
|
30
|
+
headers={"Referer": "https://example.com"},
|
|
31
|
+
)
|
|
32
|
+
]
|
|
33
|
+
return provider
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def mock_flask_app(mock_provider):
|
|
38
|
+
"""Create a mock Flask app with routes."""
|
|
39
|
+
with patch("weeb_cli.commands.serve_restful.get_provider") as mock_get_provider:
|
|
40
|
+
mock_get_provider.return_value = mock_provider
|
|
41
|
+
|
|
42
|
+
with patch("weeb_cli.commands.serve_restful.list_all_providers") as mock_list:
|
|
43
|
+
mock_list.return_value = [
|
|
44
|
+
{"name": "test_provider", "lang": "en", "region": "US"}
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Import after patching
|
|
48
|
+
from flask import Flask
|
|
49
|
+
app = Flask(__name__)
|
|
50
|
+
|
|
51
|
+
# Mock the routes (simplified for testing)
|
|
52
|
+
@app.route("/health")
|
|
53
|
+
def health():
|
|
54
|
+
from flask import jsonify
|
|
55
|
+
return jsonify({
|
|
56
|
+
"status": "ok",
|
|
57
|
+
"service": "weeb-cli-restful",
|
|
58
|
+
"providers": ["test_provider"],
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
yield app
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_health_endpoint(mock_flask_app):
|
|
65
|
+
"""Test health check endpoint."""
|
|
66
|
+
client = mock_flask_app.test_client()
|
|
67
|
+
response = client.get("/health")
|
|
68
|
+
|
|
69
|
+
assert response.status_code == 200
|
|
70
|
+
data = response.get_json()
|
|
71
|
+
assert data["status"] == "ok"
|
|
72
|
+
assert data["service"] == "weeb-cli-restful"
|
|
73
|
+
assert "test_provider" in data["providers"]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_serialize_anime_result():
|
|
77
|
+
"""Test AnimeResult serialization."""
|
|
78
|
+
from weeb_cli.commands.serve_restful import _serialize_anime_result
|
|
79
|
+
|
|
80
|
+
result = AnimeResult(
|
|
81
|
+
id="test123",
|
|
82
|
+
title="Test Anime",
|
|
83
|
+
type="series",
|
|
84
|
+
cover="https://example.com/cover.jpg",
|
|
85
|
+
year=2024,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
serialized = _serialize_anime_result(result)
|
|
89
|
+
|
|
90
|
+
assert serialized["id"] == "test123"
|
|
91
|
+
assert serialized["title"] == "Test Anime"
|
|
92
|
+
assert serialized["type"] == "series"
|
|
93
|
+
assert serialized["cover"] == "https://example.com/cover.jpg"
|
|
94
|
+
assert serialized["year"] == 2024
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_serialize_episode():
|
|
98
|
+
"""Test Episode serialization."""
|
|
99
|
+
from weeb_cli.commands.serve_restful import _serialize_episode
|
|
100
|
+
|
|
101
|
+
episode = Episode(
|
|
102
|
+
id="ep1",
|
|
103
|
+
number=1,
|
|
104
|
+
title="Episode 1",
|
|
105
|
+
season=1,
|
|
106
|
+
url="https://example.com/ep1",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
serialized = _serialize_episode(episode)
|
|
110
|
+
|
|
111
|
+
assert serialized["id"] == "ep1"
|
|
112
|
+
assert serialized["number"] == 1
|
|
113
|
+
assert serialized["title"] == "Episode 1"
|
|
114
|
+
assert serialized["season"] == 1
|
|
115
|
+
assert serialized["url"] == "https://example.com/ep1"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_serialize_stream():
|
|
119
|
+
"""Test StreamLink serialization."""
|
|
120
|
+
from weeb_cli.commands.serve_restful import _serialize_stream
|
|
121
|
+
|
|
122
|
+
stream = StreamLink(
|
|
123
|
+
url="https://example.com/stream.m3u8",
|
|
124
|
+
quality="1080p",
|
|
125
|
+
server="default",
|
|
126
|
+
headers={"Referer": "https://example.com"},
|
|
127
|
+
subtitles="https://example.com/subs.vtt",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
serialized = _serialize_stream(stream)
|
|
131
|
+
|
|
132
|
+
assert serialized["url"] == "https://example.com/stream.m3u8"
|
|
133
|
+
assert serialized["quality"] == "1080p"
|
|
134
|
+
assert serialized["server"] == "default"
|
|
135
|
+
assert serialized["headers"]["Referer"] == "https://example.com"
|
|
136
|
+
assert serialized["subtitles"] == "https://example.com/subs.vtt"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_quality_score():
|
|
140
|
+
"""Test quality scoring function."""
|
|
141
|
+
from weeb_cli.commands.serve_restful import _quality_score
|
|
142
|
+
|
|
143
|
+
assert _quality_score("4k") == 5
|
|
144
|
+
assert _quality_score("2160p") == 5
|
|
145
|
+
assert _quality_score("1080p") == 4
|
|
146
|
+
assert _quality_score("720p") == 3
|
|
147
|
+
assert _quality_score("480p") == 2
|
|
148
|
+
assert _quality_score("360p") == 1
|
|
149
|
+
assert _quality_score("unknown") == 0
|
|
150
|
+
assert _quality_score(None) == 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@patch("weeb_cli.commands.serve_restful.get_provider")
|
|
154
|
+
def test_search_with_provider(mock_get_provider, mock_provider):
|
|
155
|
+
"""Test search functionality with provider selection."""
|
|
156
|
+
mock_get_provider.return_value = mock_provider
|
|
157
|
+
|
|
158
|
+
# Simulate search
|
|
159
|
+
results = mock_provider.search("test query")
|
|
160
|
+
|
|
161
|
+
assert len(results) == 1
|
|
162
|
+
assert results[0].title == "Test Anime"
|
|
163
|
+
assert results[0].id == "test123"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@patch("weeb_cli.commands.serve_restful.get_provider")
|
|
167
|
+
def test_get_episodes_with_season_filter(mock_get_provider, mock_provider):
|
|
168
|
+
"""Test episode listing with season filter."""
|
|
169
|
+
mock_get_provider.return_value = mock_provider
|
|
170
|
+
|
|
171
|
+
# Add episodes from different seasons
|
|
172
|
+
mock_provider.get_episodes.return_value = [
|
|
173
|
+
Episode(id="ep1", number=1, season=1),
|
|
174
|
+
Episode(id="ep2", number=2, season=1),
|
|
175
|
+
Episode(id="ep3", number=1, season=2),
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
episodes = mock_provider.get_episodes("test123")
|
|
179
|
+
season_1_episodes = [ep for ep in episodes if ep.season == 1]
|
|
180
|
+
|
|
181
|
+
assert len(season_1_episodes) == 2
|
|
182
|
+
assert all(ep.season == 1 for ep in season_1_episodes)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@patch("weeb_cli.commands.serve_restful.get_provider")
|
|
186
|
+
def test_get_streams_sorted(mock_get_provider, mock_provider):
|
|
187
|
+
"""Test stream retrieval with quality sorting."""
|
|
188
|
+
from weeb_cli.commands.serve_restful import _quality_score
|
|
189
|
+
|
|
190
|
+
mock_get_provider.return_value = mock_provider
|
|
191
|
+
|
|
192
|
+
# Add streams with different qualities
|
|
193
|
+
mock_provider.get_streams.return_value = [
|
|
194
|
+
StreamLink(url="url1", quality="480p"),
|
|
195
|
+
StreamLink(url="url2", quality="1080p"),
|
|
196
|
+
StreamLink(url="url3", quality="720p"),
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
streams = mock_provider.get_streams("test123", "ep1")
|
|
200
|
+
sorted_streams = sorted(streams, key=lambda s: _quality_score(s.quality), reverse=True)
|
|
201
|
+
|
|
202
|
+
assert sorted_streams[0].quality == "1080p"
|
|
203
|
+
assert sorted_streams[1].quality == "720p"
|
|
204
|
+
assert sorted_streams[2].quality == "480p"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_missing_flask_import():
|
|
208
|
+
"""Test graceful handling when Flask is not installed."""
|
|
209
|
+
with patch("builtins.__import__", side_effect=ImportError("No module named 'flask'")):
|
|
210
|
+
with pytest.raises(ImportError):
|
|
211
|
+
import flask
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@pytest.mark.parametrize("quality,expected_score", [
|
|
215
|
+
("4K", 5),
|
|
216
|
+
("2160p", 5),
|
|
217
|
+
("1080p", 4),
|
|
218
|
+
("720p", 3),
|
|
219
|
+
("480p", 2),
|
|
220
|
+
("360p", 1),
|
|
221
|
+
("auto", 0),
|
|
222
|
+
("", 0),
|
|
223
|
+
])
|
|
224
|
+
def test_quality_score_parametrized(quality, expected_score):
|
|
225
|
+
"""Test quality scoring with various inputs."""
|
|
226
|
+
from weeb_cli.commands.serve_restful import _quality_score
|
|
227
|
+
assert _quality_score(quality) == expected_score
|
|
@@ -8,6 +8,10 @@ from weeb_cli.services.player import player
|
|
|
8
8
|
from weeb_cli.services.progress import progress_tracker
|
|
9
9
|
from weeb_cli.services.scraper import scraper
|
|
10
10
|
from weeb_cli.services.logger import error as log_error
|
|
11
|
+
from weeb_cli.services.stream_validator import stream_validator
|
|
12
|
+
from weeb_cli.config import config
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
+
from weeb_cli.services.tracker import anilist_tracker, mal_tracker, kitsu_tracker
|
|
11
15
|
from .episode_utils import get_episodes_safe, group_episodes_by_season, make_season_episode_id
|
|
12
16
|
from .stream_utils import sort_streams, extract_streams_from_response
|
|
13
17
|
|
|
@@ -148,15 +152,7 @@ def _build_episode_choices(episodes, season, completed_ids, next_ep_num):
|
|
|
148
152
|
|
|
149
153
|
return ep_choices
|
|
150
154
|
|
|
151
|
-
def
|
|
152
|
-
ep_id = selected_ep.get("id")
|
|
153
|
-
ep_num = selected_ep.get("number") or selected_ep.get("ep_num")
|
|
154
|
-
|
|
155
|
-
if not ep_id:
|
|
156
|
-
console.print(f"[red]{i18n.t('details.invalid_ep_id')}[/red]")
|
|
157
|
-
time.sleep(1)
|
|
158
|
-
return False
|
|
159
|
-
|
|
155
|
+
def _fetch_and_validate_streams(slug, ep_id):
|
|
160
156
|
with console.status(i18n.t("common.processing"), spinner="dots"):
|
|
161
157
|
stream_resp = get_streams(slug, ep_id)
|
|
162
158
|
|
|
@@ -168,24 +164,40 @@ def _play_episode(slug, selected_ep, details, season, episodes, completed_ids):
|
|
|
168
164
|
error_msg += f" [{scraper.last_error}]"
|
|
169
165
|
console.print(f"[red]{error_msg}[/red]")
|
|
170
166
|
time.sleep(1.5)
|
|
171
|
-
return
|
|
167
|
+
return None
|
|
172
168
|
|
|
173
|
-
from weeb_cli.services.stream_validator import stream_validator
|
|
174
|
-
from weeb_cli.config import config
|
|
175
|
-
|
|
176
169
|
valid_streams = []
|
|
177
170
|
if config.get("scraping_source") == "docchi":
|
|
178
171
|
valid_streams = streams_list
|
|
179
172
|
else:
|
|
180
173
|
console.print(f"[dim]{i18n.t('details.validating_streams')}...[/dim]")
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
174
|
+
|
|
175
|
+
def check_stream(stream):
|
|
176
|
+
is_valid, _ = stream_validator.validate_url(stream.get("url"), timeout=3)
|
|
177
|
+
return stream if is_valid else None
|
|
178
|
+
|
|
179
|
+
with ThreadPoolExecutor(max_workers=len(streams_list) if streams_list else 1) as executor:
|
|
180
|
+
results = executor.map(check_stream, streams_list)
|
|
181
|
+
valid_streams = [s for s in results if s is not None]
|
|
185
182
|
|
|
186
183
|
if not valid_streams:
|
|
187
184
|
console.print(f"[red]{i18n.t('details.no_valid_streams')}[/red]")
|
|
188
185
|
time.sleep(1.5)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
return valid_streams
|
|
189
|
+
|
|
190
|
+
def _play_episode(slug, selected_ep, details, season, episodes, completed_ids):
|
|
191
|
+
ep_id = selected_ep.get("id")
|
|
192
|
+
ep_num = selected_ep.get("number") or selected_ep.get("ep_num")
|
|
193
|
+
|
|
194
|
+
if not ep_id:
|
|
195
|
+
console.print(f"[red]{i18n.t('details.invalid_ep_id')}[/red]")
|
|
196
|
+
time.sleep(1)
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
valid_streams = _fetch_and_validate_streams(slug, ep_id)
|
|
200
|
+
if not valid_streams:
|
|
189
201
|
return False
|
|
190
202
|
|
|
191
203
|
streams_list = sort_streams(valid_streams)
|
|
@@ -222,7 +234,7 @@ def _play_episode(slug, selected_ep, details, season, episodes, completed_ids):
|
|
|
222
234
|
title=details.get("title"),
|
|
223
235
|
total_episodes=total_eps
|
|
224
236
|
)
|
|
225
|
-
_update_trackers(details, slug)
|
|
237
|
+
_update_trackers(details, slug, n)
|
|
226
238
|
auto_watched["triggered"] = True
|
|
227
239
|
|
|
228
240
|
play_success = player.play(
|
|
@@ -278,7 +290,7 @@ def _mark_episode_watched(slug, details, ep_num, season, episodes, completed_ids
|
|
|
278
290
|
)
|
|
279
291
|
console.print(f"[green]✓ {i18n.t('details.marked_watched')}[/green]")
|
|
280
292
|
|
|
281
|
-
_update_trackers(details, slug)
|
|
293
|
+
_update_trackers(details, slug, n)
|
|
282
294
|
|
|
283
295
|
completed_ids.add(season_ep_id)
|
|
284
296
|
return True
|
|
@@ -286,12 +298,14 @@ def _mark_episode_watched(slug, details, ep_num, season, episodes, completed_ids
|
|
|
286
298
|
log_error(f"Failed to mark episode as watched: {e}")
|
|
287
299
|
return False
|
|
288
300
|
|
|
289
|
-
def _update_trackers(details, slug):
|
|
290
|
-
from weeb_cli.services.tracker import anilist_tracker, mal_tracker, kitsu_tracker
|
|
291
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
292
|
-
|
|
301
|
+
def _update_trackers(details, slug, ep_num=None):
|
|
293
302
|
updated_prog = progress_tracker.get_anime_progress(slug)
|
|
294
|
-
|
|
303
|
+
|
|
304
|
+
if ep_num is not None:
|
|
305
|
+
total_watched = int(ep_num)
|
|
306
|
+
else:
|
|
307
|
+
total_watched = len(updated_prog.get("completed", []))
|
|
308
|
+
|
|
295
309
|
total_eps = details.get("total_episodes", 0)
|
|
296
310
|
|
|
297
311
|
trackers = [
|
|
@@ -32,10 +32,30 @@ def _sanitize_for_release(name: str) -> str:
|
|
|
32
32
|
|
|
33
33
|
serve_app = typer.Typer(
|
|
34
34
|
name="serve",
|
|
35
|
-
help="Start
|
|
35
|
+
help="Start server modes: Torznab for Sonarr/*arr integration or RESTful API server.",
|
|
36
36
|
add_completion=False,
|
|
37
|
+
invoke_without_command=True,
|
|
37
38
|
)
|
|
38
39
|
|
|
40
|
+
@serve_app.callback()
|
|
41
|
+
def serve_callback(ctx: typer.Context):
|
|
42
|
+
"""Server modes for weeb-cli.
|
|
43
|
+
|
|
44
|
+
Available modes:
|
|
45
|
+
- torznab: Torznab-compatible server for Sonarr/*arr integration
|
|
46
|
+
- restful: RESTful API server for web/mobile applications
|
|
47
|
+
"""
|
|
48
|
+
if ctx.invoked_subcommand is None:
|
|
49
|
+
typer.echo(ctx.get_help())
|
|
50
|
+
raise typer.Exit(0)
|
|
51
|
+
|
|
52
|
+
# Import restful subcommand
|
|
53
|
+
try:
|
|
54
|
+
from weeb_cli.commands.serve_restful import restful_app
|
|
55
|
+
serve_app.add_typer(restful_app, name="restful")
|
|
56
|
+
except ImportError:
|
|
57
|
+
pass # Flask not installed, restful mode unavailable
|
|
58
|
+
|
|
39
59
|
|
|
40
60
|
# -- Helpers ------------------------------------------------------------------
|
|
41
61
|
|
|
@@ -206,10 +226,16 @@ def _get_fallback_streams(fallback_providers, sonarr_title, season, episode_num)
|
|
|
206
226
|
return []
|
|
207
227
|
|
|
208
228
|
|
|
209
|
-
# --
|
|
229
|
+
# -- Torznab serve command ----------------------------------------------------
|
|
230
|
+
|
|
231
|
+
torznab_app = typer.Typer(
|
|
232
|
+
name="torznab",
|
|
233
|
+
help="Start Torznab-compatible server for Sonarr/*arr integration.",
|
|
234
|
+
add_completion=False,
|
|
235
|
+
)
|
|
210
236
|
|
|
211
|
-
@
|
|
212
|
-
def
|
|
237
|
+
@torznab_app.callback(invoke_without_command=True)
|
|
238
|
+
def serve_torznab(
|
|
213
239
|
ctx: typer.Context,
|
|
214
240
|
port: int = typer.Option(9876, "--port", envvar="FLASK_PORT", help="HTTP port"),
|
|
215
241
|
watch_dir: str = typer.Option("/downloads/weeb-cli/watch", "--watch-dir", envvar="WATCH_DIR", help="Blackhole watch directory"),
|
|
@@ -470,3 +496,7 @@ def serve(
|
|
|
470
496
|
worker.start()
|
|
471
497
|
|
|
472
498
|
flask_app.run(host="0.0.0.0", port=port, debug=False)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# Register torznab subcommand
|
|
502
|
+
serve_app.add_typer(torznab_app, name="torznab")
|