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.
Files changed (114) hide show
  1. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/PKG-INFO +38 -5
  2. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/README.md +32 -2
  3. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/pyproject.toml +4 -3
  4. weeb_cli-2.14.2/tests/test_restful_api.py +227 -0
  5. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/__init__.py +1 -1
  6. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/watch_flow.py +38 -24
  7. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/serve.py +34 -4
  8. weeb_cli-2.14.2/weeb_cli/commands/serve_restful.py +384 -0
  9. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/en/hianime.py +1 -1
  10. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/registry.py +17 -5
  11. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/PKG-INFO +38 -5
  12. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/SOURCES.txt +2 -0
  13. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/requires.txt +4 -0
  14. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/LICENSE +0 -0
  15. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/setup.cfg +0 -0
  16. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_anilist_tracker.py +0 -0
  17. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_aniworld_provider.py +0 -0
  18. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_api.py +0 -0
  19. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_cache.py +0 -0
  20. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_exceptions.py +0 -0
  21. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_kitsu_tracker.py +0 -0
  22. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_mal_tracker.py +0 -0
  23. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_providers.py +0 -0
  24. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_sanitizer.py +0 -0
  25. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/tests/test_sanitizer_security.py +0 -0
  26. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/__main__.py +0 -0
  27. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/__init__.py +0 -0
  28. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/api.py +0 -0
  29. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/downloads.py +0 -0
  30. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/library.py +0 -0
  31. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/__init__.py +0 -0
  32. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/anime_details.py +0 -0
  33. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/download_flow.py +0 -0
  34. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/episode_utils.py +0 -0
  35. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/search_handlers.py +0 -0
  36. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search/stream_utils.py +0 -0
  37. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/search.py +0 -0
  38. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/__init__.py +0 -0
  39. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_backup.py +0 -0
  40. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_cache.py +0 -0
  41. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_config.py +0 -0
  42. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_download.py +0 -0
  43. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_drives.py +0 -0
  44. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_menu.py +0 -0
  45. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_shortcuts.py +0 -0
  46. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings/settings_trackers.py +0 -0
  47. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/settings.py +0 -0
  48. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/setup.py +0 -0
  49. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/commands/watchlist.py +0 -0
  50. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/config.py +0 -0
  51. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/exceptions.py +0 -0
  52. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/i18n.py +0 -0
  53. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/locales/de.json +0 -0
  54. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/locales/en.json +0 -0
  55. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/locales/pl.json +0 -0
  56. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/locales/tr.json +0 -0
  57. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/main.py +0 -0
  58. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/__init__.py +0 -0
  59. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/base.py +0 -0
  60. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/de/__init__.py +0 -0
  61. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/de/aniworld.py +0 -0
  62. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/en/__init__.py +0 -0
  63. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/en/allanime.py +0 -0
  64. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/__init__.py +0 -0
  65. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/doodstream.py +0 -0
  66. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/filemoon.py +0 -0
  67. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/megacloud.py +0 -0
  68. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/streamtape.py +0 -0
  69. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/vidoza.py +0 -0
  70. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/extractors/voe.py +0 -0
  71. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/pl/__init__.py +0 -0
  72. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/pl/docchi.py +0 -0
  73. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/tr/__init__.py +0 -0
  74. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/tr/animecix.py +0 -0
  75. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/tr/anizle.py +0 -0
  76. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/tr/turkanime.py +0 -0
  77. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/providers/tr/weeb.py +0 -0
  78. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/__init__.py +0 -0
  79. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/_base.py +0 -0
  80. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/_tracker_base.py +0 -0
  81. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/aniskip.py +0 -0
  82. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/cache.py +0 -0
  83. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/database.py +0 -0
  84. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/dependency_manager.py +0 -0
  85. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/details.py +0 -0
  86. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/discord_rpc.py +0 -0
  87. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/downloader.py +0 -0
  88. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/error_handler.py +0 -0
  89. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/headless_downloader.py +0 -0
  90. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/local_library.py +0 -0
  91. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/logger.py +0 -0
  92. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/notifier.py +0 -0
  93. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/player.py +0 -0
  94. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/progress.py +0 -0
  95. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/scraper.py +0 -0
  96. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/search.py +0 -0
  97. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/shortcuts.py +0 -0
  98. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/stream_validator.py +0 -0
  99. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/tracker.py +0 -0
  100. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/updater.py +0 -0
  101. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/services/watch.py +0 -0
  102. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/templates/anilist_error.html +0 -0
  103. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/templates/anilist_success.html +0 -0
  104. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/templates/mal_error.html +0 -0
  105. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/templates/mal_success.html +0 -0
  106. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/ui/__init__.py +0 -0
  107. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/ui/header.py +0 -0
  108. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/ui/menu.py +0 -0
  109. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/ui/prompt.py +0 -0
  110. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/utils/__init__.py +0 -0
  111. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli/utils/sanitizer.py +0 -0
  112. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/dependency_links.txt +0 -0
  113. {weeb_cli-2.13.2 → weeb_cli-2.14.2}/weeb_cli.egg-info/entry_points.txt +0 -0
  114. {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.13.2
4
- Summary: Tarayıcı yok, reklam yok, dikkat dağıtıcı unsur yok. Sadece siz ve eşsiz bir anime izleme deneyimi.
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,weeb,anime-download,anizm,anime-watch,anime-watching,anime-downloading,anime-cli,allanime,animecix,anime-indir,anime-izle,weeb-cli,anime-izleme,anime-indirme,weebanime,tranime
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
- #### Docker
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
- #### Docker
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.13.2"
8
- description = "Tarayıcı yok, reklam yok, dikkat dağıtıcı unsur yok. Sadece siz ve eşsiz bir anime izleme deneyimi."
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", "weeb", "anime-download", "anizm", "anime-watch", "anime-watching", "anime-downloading", "anime-cli", "allanime", "animecix", "anime-indir", "anime-izle", "weeb-cli", "anime-izleme", "anime-indirme", "weebanime", "tranime"]
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
@@ -22,4 +22,4 @@ Attributes:
22
22
  __version__ (str): Current version of Weeb CLI.
23
23
  """
24
24
 
25
- __version__ = "2.13.2"
25
+ __version__ = "2.14.2"
@@ -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 _play_episode(slug, selected_ep, details, season, episodes, completed_ids):
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 False
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
- for stream in streams_list:
182
- is_valid, error = stream_validator.validate_url(stream.get("url"), timeout=3)
183
- if is_valid:
184
- valid_streams.append(stream)
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
- total_watched = len(updated_prog.get("completed", []))
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 a Torznab-compatible server for Sonarr/*arr integration.",
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
- # -- Main serve command -------------------------------------------------------
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
- @serve_app.callback(invoke_without_command=True)
212
- def serve(
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")