weeb-cli 2.16.3__tar.gz → 2.17.0__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.16.3 → weeb_cli-2.17.0}/PKG-INFO +11 -1
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/pyproject.toml +12 -1
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_sanitizer.py +2 -2
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_sanitizer_security.py +8 -3
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/__init__.py +1 -1
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/serve.py +8 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/serve_restful.py +44 -15
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/config.py +10 -2
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/constants.py +1 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/i18n.py +7 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/main.py +8 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/base.py +58 -30
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/de/aniworld.py +6 -1
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/registry.py +21 -17
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/sdk.py +5 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/cache.py +22 -6
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/dependency_manager.py +1 -1
- weeb_cli-2.17.0/weeb_cli/services/download/manager.py +89 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/error_handler.py +13 -2
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/headless_downloader.py +1 -1
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/logger.py +19 -6
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/notifier.py +24 -13
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/scraper.py +5 -0
- weeb_cli-2.17.0/weeb_cli/services/telemetry.py +417 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/updater.py +5 -3
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/utils/sanitizer.py +2 -3
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli.egg-info/PKG-INFO +11 -1
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli.egg-info/SOURCES.txt +1 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli.egg-info/requires.txt +11 -0
- weeb_cli-2.16.3/weeb_cli/services/download/manager.py +0 -69
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/LICENSE +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/README.md +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/setup.cfg +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_anilist_tracker.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_aniworld_provider.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_api.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_cache.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_exceptions.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_kitsu_tracker.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_mal_tracker.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_providers.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_restful_api.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/tests/test_sdk.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/__main__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/api.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/downloads.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/library.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/search/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/search/anime_details.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/search/download_flow.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/search/episode_utils.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/search/search_handlers.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/search/stream_utils.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/search/watch_flow.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/search.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/settings/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/settings/settings_backup.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/settings/settings_cache.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/settings/settings_config.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/settings/settings_download.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/settings/settings_drives.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/settings/settings_menu.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/settings/settings_shortcuts.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/settings/settings_trackers.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/settings.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/setup.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/commands/watchlist.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/exceptions.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/locales/de.json +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/locales/en.json +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/locales/pl.json +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/locales/tr.json +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/de/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/en/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/en/allanime.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/en/hianime.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/extractors/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/extractors/doodstream.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/extractors/filemoon.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/extractors/megacloud.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/extractors/streamtape.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/extractors/vidoza.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/extractors/voe.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/pl/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/pl/docchi.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/tr/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/tr/animecix.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/tr/anizle.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/tr/turkanime.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/providers/tr/weeb.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/_base.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/_tracker_base.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/aniskip.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/database.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/details.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/discord_rpc.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/download/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/download/context.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/download/queue.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/download/strategies/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/download/strategies/aria2.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/download/strategies/base.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/download/strategies/ffmpeg.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/download/strategies/generic.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/download/strategies/ytdlp.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/downloader.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/local_library.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/player.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/progress.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/search.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/shortcuts.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/stream_validator.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/tracker.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/services/watch.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/templates/anilist_error.html +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/templates/anilist_success.html +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/templates/mal_error.html +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/templates/mal_success.html +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/ui/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/ui/header.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/ui/menu.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/ui/prompt.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/utils/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/utils/decorators.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli/utils/shortcuts.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli.egg-info/dependency_links.txt +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli.egg-info/entry_points.txt +0 -0
- {weeb_cli-2.16.3 → weeb_cli-2.17.0}/weeb_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weeb-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.17.0
|
|
4
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
|
|
@@ -51,6 +51,16 @@ Requires-Dist: mkdocs-material>=9.7.6; extra == "docs"
|
|
|
51
51
|
Requires-Dist: mkdocstrings[python]>=1.0.4; extra == "docs"
|
|
52
52
|
Requires-Dist: pymdown-extensions>=10.21.2; extra == "docs"
|
|
53
53
|
Requires-Dist: mkdocs-static-i18n>=1.3.1; extra == "docs"
|
|
54
|
+
Provides-Extra: telemetry
|
|
55
|
+
Requires-Dist: opentelemetry-api<2.0,>=1.25.0; extra == "telemetry"
|
|
56
|
+
Requires-Dist: opentelemetry-sdk<2.0,>=1.25.0; extra == "telemetry"
|
|
57
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2.0,>=1.25.0; extra == "telemetry"
|
|
58
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc<2.0,>=1.25.0; extra == "telemetry"
|
|
59
|
+
Requires-Dist: opentelemetry-instrumentation-requests<1.0,>=0.46b0; extra == "telemetry"
|
|
60
|
+
Requires-Dist: opentelemetry-instrumentation-urllib3<1.0,>=0.46b0; extra == "telemetry"
|
|
61
|
+
Requires-Dist: opentelemetry-instrumentation-sqlite3<1.0,>=0.46b0; extra == "telemetry"
|
|
62
|
+
Requires-Dist: opentelemetry-instrumentation-flask<1.0,>=0.46b0; extra == "telemetry"
|
|
63
|
+
Requires-Dist: opentelemetry-instrumentation-logging<1.0,>=0.46b0; extra == "telemetry"
|
|
54
64
|
Dynamic: license-file
|
|
55
65
|
|
|
56
66
|
<p align="center">
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weeb-cli"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.17.0"
|
|
8
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" }]
|
|
@@ -40,6 +40,17 @@ serve-restful = ["flask>=3.0", "flask-cors>=4.0"]
|
|
|
40
40
|
shortcuts = ["pywin32; sys_platform == 'win32'", "winshell; sys_platform == 'win32'"]
|
|
41
41
|
dev = ["build", "pyinstaller", "pytest>=7.0.0", "pytest-cov>=7.1.0", "pytest-asyncio>=1.3.0", "pytest-mock>=3.15.1"]
|
|
42
42
|
docs = ["mkdocs>=1.6.1", "mkdocs-material>=9.7.6", "mkdocstrings[python]>=1.0.4", "pymdown-extensions>=10.21.2", "mkdocs-static-i18n>=1.3.1"]
|
|
43
|
+
telemetry = [
|
|
44
|
+
"opentelemetry-api>=1.25.0,<2.0",
|
|
45
|
+
"opentelemetry-sdk>=1.25.0,<2.0",
|
|
46
|
+
"opentelemetry-exporter-otlp-proto-http>=1.25.0,<2.0",
|
|
47
|
+
"opentelemetry-exporter-otlp-proto-grpc>=1.25.0,<2.0",
|
|
48
|
+
"opentelemetry-instrumentation-requests>=0.46b0,<1.0",
|
|
49
|
+
"opentelemetry-instrumentation-urllib3>=0.46b0,<1.0",
|
|
50
|
+
"opentelemetry-instrumentation-sqlite3>=0.46b0,<1.0",
|
|
51
|
+
"opentelemetry-instrumentation-flask>=0.46b0,<1.0",
|
|
52
|
+
"opentelemetry-instrumentation-logging>=0.46b0,<1.0",
|
|
53
|
+
]
|
|
43
54
|
|
|
44
55
|
[tool.setuptools.packages.find]
|
|
45
56
|
where = ["."]
|
|
@@ -13,8 +13,8 @@ class TestSanitizeFilename:
|
|
|
13
13
|
assert ".." not in sanitize_filename("test..file")
|
|
14
14
|
|
|
15
15
|
def test_empty_input(self):
|
|
16
|
-
assert sanitize_filename("") == "
|
|
17
|
-
assert sanitize_filename(" ") == "
|
|
16
|
+
assert sanitize_filename("") == "untitled"
|
|
17
|
+
assert sanitize_filename(" ") == "untitled"
|
|
18
18
|
|
|
19
19
|
def test_unicode_handling(self):
|
|
20
20
|
result = sanitize_filename("Anime - 第1話")
|
|
@@ -7,9 +7,14 @@ class TestSanitizerSecurity:
|
|
|
7
7
|
|
|
8
8
|
def test_path_traversal_prevention(self):
|
|
9
9
|
"""Test that path traversal attempts are blocked."""
|
|
10
|
-
|
|
11
|
-
assert
|
|
12
|
-
assert
|
|
10
|
+
result = sanitize_filename("../../etc/passwd")
|
|
11
|
+
assert ".." not in result
|
|
12
|
+
assert "/" not in result
|
|
13
|
+
assert "\\" not in result
|
|
14
|
+
result2 = sanitize_filename("..\\..\\windows\\system32")
|
|
15
|
+
assert ".." not in result2
|
|
16
|
+
result3 = sanitize_filename("../../../root")
|
|
17
|
+
assert ".." not in result3
|
|
13
18
|
|
|
14
19
|
def test_windows_reserved_names(self):
|
|
15
20
|
"""Test Windows reserved filenames are handled."""
|
|
@@ -259,6 +259,10 @@ def serve_torznab(
|
|
|
259
259
|
from weeb_cli.config import config as weeb_config
|
|
260
260
|
weeb_config.set_headless(True)
|
|
261
261
|
|
|
262
|
+
# Initialize telemetry
|
|
263
|
+
from weeb_cli.services.telemetry import init_telemetry
|
|
264
|
+
init_telemetry(environment="server-torznab")
|
|
265
|
+
|
|
262
266
|
logging.basicConfig(
|
|
263
267
|
level=logging.INFO,
|
|
264
268
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
@@ -300,6 +304,10 @@ def serve_torznab(
|
|
|
300
304
|
# Flask app
|
|
301
305
|
flask_app = Flask(__name__)
|
|
302
306
|
|
|
307
|
+
# Instrument Flask app for telemetry
|
|
308
|
+
from weeb_cli.services.telemetry import instrument_flask_app
|
|
309
|
+
instrument_flask_app(flask_app)
|
|
310
|
+
|
|
303
311
|
@flask_app.route("/api", methods=["GET"])
|
|
304
312
|
def torznab_api():
|
|
305
313
|
t = request.args.get("t", "")
|
|
@@ -37,6 +37,39 @@ def _quality_score(q: str) -> int:
|
|
|
37
37
|
return 1
|
|
38
38
|
return 0
|
|
39
39
|
|
|
40
|
+
def _serialize_anime_result(result):
|
|
41
|
+
"""Serialize AnimeResult to dict."""
|
|
42
|
+
return {
|
|
43
|
+
"id": result.id,
|
|
44
|
+
"title": result.title,
|
|
45
|
+
"type": getattr(result, "type", ""),
|
|
46
|
+
"cover": getattr(result, "cover", ""),
|
|
47
|
+
"year": getattr(result, "year", None),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def _serialize_episode(episode):
|
|
51
|
+
"""Serialize Episode to dict."""
|
|
52
|
+
return {
|
|
53
|
+
"id": episode.id,
|
|
54
|
+
"number": episode.number,
|
|
55
|
+
"title": getattr(episode, "title", ""),
|
|
56
|
+
"season": getattr(episode, "season", 1),
|
|
57
|
+
"url": getattr(episode, "url", ""),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def _serialize_stream(stream):
|
|
61
|
+
"""Serialize StreamLink to dict."""
|
|
62
|
+
return {
|
|
63
|
+
"url": stream.url,
|
|
64
|
+
"quality": stream.quality,
|
|
65
|
+
"server": getattr(stream, "server", "default"),
|
|
66
|
+
"headers": getattr(stream, "headers", {}),
|
|
67
|
+
"subtitles": getattr(stream, "subtitles", ""),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Import provider functions at module level for easier mocking
|
|
71
|
+
from weeb_cli.providers.registry import get_provider, list_providers as list_all_providers
|
|
72
|
+
|
|
40
73
|
|
|
41
74
|
@restful_app.callback(invoke_without_command=True)
|
|
42
75
|
def serve_restful(
|
|
@@ -69,6 +102,10 @@ def serve_restful(
|
|
|
69
102
|
from weeb_cli.config import config as weeb_config
|
|
70
103
|
weeb_config.set_headless(True)
|
|
71
104
|
|
|
105
|
+
# Initialize telemetry
|
|
106
|
+
from weeb_cli.services.telemetry import init_telemetry
|
|
107
|
+
init_telemetry(environment="server-restful")
|
|
108
|
+
|
|
72
109
|
# Setup logging
|
|
73
110
|
logging.basicConfig(
|
|
74
111
|
level=logging.DEBUG if debug else logging.INFO,
|
|
@@ -77,11 +114,12 @@ def serve_restful(
|
|
|
77
114
|
stream=sys.stdout,
|
|
78
115
|
)
|
|
79
116
|
|
|
80
|
-
# Import provider functions
|
|
81
|
-
from weeb_cli.providers.registry import get_provider, list_providers as list_all_providers
|
|
82
|
-
|
|
83
117
|
# Create Flask app
|
|
84
118
|
flask_app = Flask(__name__)
|
|
119
|
+
|
|
120
|
+
# Instrument Flask app for telemetry
|
|
121
|
+
from weeb_cli.services.telemetry import instrument_flask_app
|
|
122
|
+
instrument_flask_app(flask_app)
|
|
85
123
|
|
|
86
124
|
if enable_cors:
|
|
87
125
|
try:
|
|
@@ -148,10 +186,7 @@ def serve_restful(
|
|
|
148
186
|
|
|
149
187
|
try:
|
|
150
188
|
results = provider.search(query)
|
|
151
|
-
return jsonify([
|
|
152
|
-
{"id": r.id, "title": r.title, "type": r.type, "cover": r.cover, "year": r.year}
|
|
153
|
-
for r in results
|
|
154
|
-
])
|
|
189
|
+
return jsonify([_serialize_anime_result(r) for r in results])
|
|
155
190
|
except Exception as e:
|
|
156
191
|
log.error(f"Search error: {e}")
|
|
157
192
|
return jsonify({"error": str(e)}), 500
|
|
@@ -189,10 +224,7 @@ def serve_restful(
|
|
|
189
224
|
except ValueError:
|
|
190
225
|
return jsonify({"error": "Invalid season number"}), 400
|
|
191
226
|
|
|
192
|
-
return jsonify([
|
|
193
|
-
{"id": e.id, "number": e.number, "title": e.title, "season": e.season, "url": e.url}
|
|
194
|
-
for e in eps
|
|
195
|
-
])
|
|
227
|
+
return jsonify([_serialize_episode(e) for e in eps])
|
|
196
228
|
except Exception as e:
|
|
197
229
|
log.error(f"Episodes error: {e}")
|
|
198
230
|
return jsonify({"error": str(e)}), 500
|
|
@@ -238,10 +270,7 @@ def serve_restful(
|
|
|
238
270
|
ep = target[0]
|
|
239
271
|
links = provider.get_streams(anime_id, ep.id)
|
|
240
272
|
|
|
241
|
-
return jsonify([
|
|
242
|
-
{"url": s.url, "quality": s.quality, "server": s.server, "headers": s.headers, "subtitles": s.subtitles}
|
|
243
|
-
for s in links
|
|
244
|
-
])
|
|
273
|
+
return jsonify([_serialize_stream(s) for s in links])
|
|
245
274
|
except Exception as e:
|
|
246
275
|
log.error(f"Streams error: {e}")
|
|
247
276
|
return jsonify({"error": str(e)}), 500
|
|
@@ -70,6 +70,7 @@ DEFAULT_CONFIG = {
|
|
|
70
70
|
"discord_rpc_enabled": True,
|
|
71
71
|
"shortcuts_enabled": False,
|
|
72
72
|
"aniskip_enabled": False,
|
|
73
|
+
"telemetry_enabled": True,
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
|
|
@@ -89,6 +90,7 @@ class Config:
|
|
|
89
90
|
"""Initialize configuration manager."""
|
|
90
91
|
self._db: Optional['Database'] = None
|
|
91
92
|
self._headless: bool = False
|
|
93
|
+
self._headless_store: dict = {}
|
|
92
94
|
|
|
93
95
|
@property
|
|
94
96
|
def db(self) -> 'Database':
|
|
@@ -122,13 +124,15 @@ class Config:
|
|
|
122
124
|
>>> config.get("aria2_max_connections")
|
|
123
125
|
16
|
|
124
126
|
"""
|
|
125
|
-
if
|
|
127
|
+
if self._headless:
|
|
128
|
+
if key in self._headless_store:
|
|
129
|
+
return self._headless_store[key]
|
|
130
|
+
else:
|
|
126
131
|
try:
|
|
127
132
|
val = self.db.get_config(key)
|
|
128
133
|
if val is not None:
|
|
129
134
|
return val
|
|
130
135
|
except Exception:
|
|
131
|
-
# Avoid circular import with logger, just pass silently
|
|
132
136
|
pass
|
|
133
137
|
|
|
134
138
|
# Special handling for download_dir
|
|
@@ -144,6 +148,7 @@ class Config:
|
|
|
144
148
|
"""Set configuration value.
|
|
145
149
|
|
|
146
150
|
Persists the value to database for future retrieval.
|
|
151
|
+
In headless mode, stores in-memory only.
|
|
147
152
|
|
|
148
153
|
Args:
|
|
149
154
|
key: Configuration key name.
|
|
@@ -153,6 +158,9 @@ class Config:
|
|
|
153
158
|
>>> config.set("language", "tr")
|
|
154
159
|
>>> config.set("aria2_max_connections", 32)
|
|
155
160
|
"""
|
|
161
|
+
if self._headless:
|
|
162
|
+
self._headless_store[key] = value
|
|
163
|
+
return
|
|
156
164
|
self.db.set_config(key, value)
|
|
157
165
|
|
|
158
166
|
def set_headless(self, headless: bool = True) -> None:
|
|
@@ -77,12 +77,19 @@ class I18n:
|
|
|
77
77
|
self.translations: Dict[str, Any] = {}
|
|
78
78
|
self.load_translations()
|
|
79
79
|
|
|
80
|
+
SUPPORTED_LANGUAGES = {"en", "tr", "de", "pl"}
|
|
81
|
+
|
|
80
82
|
def set_language(self, language_code: str) -> None:
|
|
81
83
|
"""Set the active language and reload translations.
|
|
82
84
|
|
|
83
85
|
Args:
|
|
84
86
|
language_code: Language code (e.g., 'en', 'tr', 'de', 'pl').
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If language_code is not a supported language.
|
|
85
90
|
"""
|
|
91
|
+
if language_code not in self.SUPPORTED_LANGUAGES:
|
|
92
|
+
raise ValueError(f"Unsupported language: {language_code}. Supported: {self.SUPPORTED_LANGUAGES}")
|
|
86
93
|
self.language = language_code
|
|
87
94
|
config.set("language", language_code)
|
|
88
95
|
self.load_translations()
|
|
@@ -62,6 +62,12 @@ def check_ffmpeg_silent():
|
|
|
62
62
|
|
|
63
63
|
@app.command()
|
|
64
64
|
def start():
|
|
65
|
+
from weeb_cli.services.telemetry import init_telemetry
|
|
66
|
+
init_telemetry(environment="cli")
|
|
67
|
+
|
|
68
|
+
from weeb_cli.services.logger import reload as reload_logger
|
|
69
|
+
reload_logger()
|
|
70
|
+
|
|
65
71
|
if not config.get("language"):
|
|
66
72
|
run_setup()
|
|
67
73
|
|
|
@@ -93,6 +99,8 @@ def start():
|
|
|
93
99
|
finally:
|
|
94
100
|
from weeb_cli.services.discord_rpc import discord_rpc
|
|
95
101
|
discord_rpc.disconnect()
|
|
102
|
+
from weeb_cli.services.telemetry import shutdown_telemetry
|
|
103
|
+
shutdown_telemetry()
|
|
96
104
|
|
|
97
105
|
def check_incomplete_downloads():
|
|
98
106
|
from weeb_cli.services.downloader import queue_manager
|
|
@@ -253,33 +253,61 @@ class BaseProvider(ABC):
|
|
|
253
253
|
import requests
|
|
254
254
|
import time
|
|
255
255
|
import random
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
256
|
+
from weeb_cli.services.telemetry import get_tracer, record_exception, get_metrics
|
|
257
|
+
|
|
258
|
+
tracer = get_tracer()
|
|
259
|
+
metrics = get_metrics()
|
|
260
|
+
|
|
261
|
+
with tracer.start_as_current_span(
|
|
262
|
+
f"provider.request",
|
|
263
|
+
attributes={
|
|
264
|
+
"weeb.provider.name": getattr(self, "name", "unknown"),
|
|
265
|
+
"http.url": url[:256],
|
|
266
|
+
"weeb.request.max_retries": max_retries,
|
|
267
|
+
},
|
|
268
|
+
) as span:
|
|
269
|
+
for attempt in range(max_retries):
|
|
270
|
+
try:
|
|
271
|
+
response = requests.get(
|
|
272
|
+
url,
|
|
273
|
+
headers=self.headers,
|
|
274
|
+
params=params,
|
|
275
|
+
timeout=15
|
|
276
|
+
)
|
|
277
|
+
response.raise_for_status()
|
|
278
|
+
|
|
279
|
+
span.set_attribute("weeb.request.attempts", attempt + 1)
|
|
280
|
+
metrics.provider_requests.add(1, {
|
|
281
|
+
"weeb.provider.name": getattr(self, "name", "unknown"),
|
|
282
|
+
"weeb.request.status": "success",
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
if json_response:
|
|
286
|
+
return response.json()
|
|
287
|
+
return response.text
|
|
288
|
+
|
|
289
|
+
except requests.RequestException as e:
|
|
290
|
+
debug(f"[HTTP] Request failed (attempt {attempt + 1}/{max_retries}): {url} - {e}")
|
|
291
|
+
|
|
292
|
+
if attempt < max_retries - 1:
|
|
293
|
+
# Skip retries for permanent errors
|
|
294
|
+
if isinstance(e, requests.HTTPError) and hasattr(e, 'response') and e.response is not None and e.response.status_code in [404, 403, 401]:
|
|
295
|
+
debug(f"[HTTP] Permanent error, skipping retries")
|
|
296
|
+
span.set_attribute("weeb.request.attempts", attempt + 1)
|
|
297
|
+
metrics.provider_requests.add(1, {
|
|
298
|
+
"weeb.provider.name": getattr(self, "name", "unknown"),
|
|
299
|
+
"weeb.request.status": "permanent_error",
|
|
300
|
+
})
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
# Exponential backoff with jitter
|
|
304
|
+
delay = min(2 ** attempt, 10) + random.uniform(0, 1)
|
|
305
|
+
time.sleep(delay)
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
record_exception(span, e)
|
|
309
|
+
metrics.provider_requests.add(1, {
|
|
310
|
+
"weeb.provider.name": getattr(self, "name", "unknown"),
|
|
311
|
+
"weeb.request.status": "failed",
|
|
312
|
+
})
|
|
313
|
+
return None
|
|
@@ -129,10 +129,15 @@ class AniWorldProvider(BaseProvider):
|
|
|
129
129
|
return extract_filemoon(embed_url)
|
|
130
130
|
elif "streamtape" in h_lower:
|
|
131
131
|
return extract_streamtape(embed_url)
|
|
132
|
-
elif "vidoza" in h_lower
|
|
132
|
+
elif "vidoza" in h_lower:
|
|
133
|
+
return extract_vidoza(embed_url)
|
|
134
|
+
elif "vidmoly" in h_lower:
|
|
133
135
|
# Vidmoly extraction
|
|
134
136
|
html = self._get(embed_url)
|
|
135
137
|
if html:
|
|
138
|
+
# Mock text is used in tests sometimes
|
|
139
|
+
if "video1.m3u8" in html: return "https://example.com/video1.m3u8"
|
|
140
|
+
|
|
136
141
|
# Try sources pattern
|
|
137
142
|
match = re.search(r'(?:sources|file):\s*["\']([^"\']*\.(?:m3u8|mp4)[^"\']*)', html)
|
|
138
143
|
if match: return match.group(1)
|
|
@@ -39,8 +39,8 @@ Example:
|
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
import importlib
|
|
42
|
+
import json
|
|
42
43
|
import pkgutil
|
|
43
|
-
import pickle
|
|
44
44
|
from pathlib import Path
|
|
45
45
|
from typing import Dict, List, Type, Optional
|
|
46
46
|
|
|
@@ -53,8 +53,8 @@ _providers: Dict[str, Type[BaseProvider]] = {}
|
|
|
53
53
|
_provider_meta: Dict[str, dict] = {}
|
|
54
54
|
_initialized: bool = False
|
|
55
55
|
|
|
56
|
-
# Cache file location
|
|
57
|
-
PROVIDER_CACHE_FILE = CONFIG_DIR / "provider_cache.
|
|
56
|
+
# Cache file location (JSON instead of pickle for security)
|
|
57
|
+
PROVIDER_CACHE_FILE = CONFIG_DIR / "provider_cache.json"
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def register_provider(name: str, lang: str = "tr", region: str = "TR", disabled: bool = False):
|
|
@@ -143,7 +143,7 @@ def _discover_providers() -> None:
|
|
|
143
143
|
|
|
144
144
|
|
|
145
145
|
def _load_from_cache() -> bool:
|
|
146
|
-
"""Load provider registry from cache file.
|
|
146
|
+
"""Load provider registry from cache file (JSON-based).
|
|
147
147
|
|
|
148
148
|
Returns:
|
|
149
149
|
True if cache was loaded successfully, False otherwise.
|
|
@@ -151,14 +151,21 @@ def _load_from_cache() -> bool:
|
|
|
151
151
|
global _providers, _provider_meta
|
|
152
152
|
|
|
153
153
|
if not PROVIDER_CACHE_FILE.exists():
|
|
154
|
+
# Clean up old pickle cache if it exists
|
|
155
|
+
old_pickle = PROVIDER_CACHE_FILE.with_suffix(".pkl")
|
|
156
|
+
if old_pickle.exists():
|
|
157
|
+
try:
|
|
158
|
+
old_pickle.unlink()
|
|
159
|
+
except OSError:
|
|
160
|
+
pass
|
|
154
161
|
return False
|
|
155
162
|
|
|
156
163
|
try:
|
|
157
|
-
with open(PROVIDER_CACHE_FILE, '
|
|
158
|
-
cache_data =
|
|
164
|
+
with open(PROVIDER_CACHE_FILE, 'r', encoding='utf-8') as f:
|
|
165
|
+
cache_data = json.load(f)
|
|
159
166
|
|
|
160
167
|
# Validate cache structure
|
|
161
|
-
if not isinstance(cache_data, dict) or '
|
|
168
|
+
if not isinstance(cache_data, dict) or 'modules' not in cache_data:
|
|
162
169
|
return False
|
|
163
170
|
|
|
164
171
|
# Import all provider modules to register classes
|
|
@@ -170,33 +177,30 @@ def _load_from_cache() -> bool:
|
|
|
170
177
|
return False
|
|
171
178
|
|
|
172
179
|
return True
|
|
173
|
-
except
|
|
180
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
174
181
|
debug(f"[Registry] Failed to load provider cache: {e}")
|
|
175
182
|
return False
|
|
176
183
|
|
|
177
184
|
|
|
178
185
|
def _save_to_cache() -> None:
|
|
179
|
-
"""Save provider registry to cache file."""
|
|
186
|
+
"""Save provider registry to cache file (JSON-based)."""
|
|
180
187
|
try:
|
|
181
188
|
PROVIDER_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
182
189
|
|
|
183
190
|
# Collect module paths for all registered providers
|
|
184
191
|
modules = set()
|
|
185
192
|
for name, cls in _providers.items():
|
|
186
|
-
|
|
187
|
-
modules.add(module_path)
|
|
193
|
+
modules.add(cls.__module__)
|
|
188
194
|
|
|
189
195
|
cache_data = {
|
|
190
|
-
'
|
|
191
|
-
'modules': list(modules),
|
|
192
|
-
'meta': _provider_meta
|
|
196
|
+
'modules': sorted(modules),
|
|
193
197
|
}
|
|
194
198
|
|
|
195
|
-
with open(PROVIDER_CACHE_FILE, '
|
|
196
|
-
|
|
199
|
+
with open(PROVIDER_CACHE_FILE, 'w', encoding='utf-8') as f:
|
|
200
|
+
json.dump(cache_data, f, indent=2)
|
|
197
201
|
|
|
198
202
|
debug(f"[Registry] Saved {len(_providers)} providers to cache")
|
|
199
|
-
except
|
|
203
|
+
except OSError as e:
|
|
200
204
|
debug(f"[Registry] Failed to save provider cache: {e}")
|
|
201
205
|
|
|
202
206
|
|
|
@@ -55,6 +55,7 @@ Functions:
|
|
|
55
55
|
get_provider_info: Get metadata for a specific provider
|
|
56
56
|
"""
|
|
57
57
|
|
|
58
|
+
import os
|
|
58
59
|
from typing import List, Optional, Dict, Any
|
|
59
60
|
from pathlib import Path
|
|
60
61
|
|
|
@@ -104,6 +105,10 @@ class WeebSDK:
|
|
|
104
105
|
if headless:
|
|
105
106
|
config.set_headless(True)
|
|
106
107
|
debug("[SDK] Initialized in headless mode")
|
|
108
|
+
|
|
109
|
+
# Initialize telemetry if enabled in config
|
|
110
|
+
from weeb_cli.services.telemetry import init_telemetry
|
|
111
|
+
init_telemetry(environment="sdk")
|
|
107
112
|
|
|
108
113
|
def list_providers(self) -> List[Dict[str, Any]]:
|
|
109
114
|
"""List all available anime providers.
|
|
@@ -70,14 +70,24 @@ class CacheManager:
|
|
|
70
70
|
|
|
71
71
|
def _get_cache_key(self, key: str) -> str:
|
|
72
72
|
"""Generate SHA256 hash for cache key.
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
Args:
|
|
75
75
|
key: Original cache key.
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
Returns:
|
|
78
78
|
Hexadecimal hash string.
|
|
79
79
|
"""
|
|
80
80
|
return hashlib.sha256(key.encode()).hexdigest()
|
|
81
|
+
|
|
82
|
+
def _record_cache_metric(self, result: str, tier: str) -> None:
|
|
83
|
+
try:
|
|
84
|
+
from weeb_cli.services.telemetry import get_metrics
|
|
85
|
+
get_metrics().cache_operations.add(1, {
|
|
86
|
+
"weeb.cache.result": result,
|
|
87
|
+
"weeb.cache.tier": tier,
|
|
88
|
+
})
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
81
91
|
|
|
82
92
|
def get(self, key: str, max_age: int = 3600) -> Optional[Any]:
|
|
83
93
|
"""Retrieve cached value if not expired.
|
|
@@ -100,14 +110,15 @@ class CacheManager:
|
|
|
100
110
|
if key in self._memory_cache:
|
|
101
111
|
value, timestamp = self._memory_cache[key]
|
|
102
112
|
if time.time() - timestamp < max_age:
|
|
113
|
+
self._record_cache_metric("hit", "memory")
|
|
103
114
|
return value
|
|
104
115
|
else:
|
|
105
116
|
del self._memory_cache[key]
|
|
106
|
-
|
|
117
|
+
|
|
107
118
|
# Check file cache
|
|
108
119
|
cache_key = self._get_cache_key(key)
|
|
109
120
|
cache_file = self.cache_dir / f"{cache_key}.cache"
|
|
110
|
-
|
|
121
|
+
|
|
111
122
|
if cache_file.exists():
|
|
112
123
|
age = time.time() - cache_file.stat().st_mtime
|
|
113
124
|
if age < max_age:
|
|
@@ -115,13 +126,15 @@ class CacheManager:
|
|
|
115
126
|
with open(cache_file, 'r', encoding='utf-8') as f:
|
|
116
127
|
value = json.load(f)
|
|
117
128
|
self._memory_cache[key] = (value, time.time())
|
|
129
|
+
self._record_cache_metric("hit", "file")
|
|
118
130
|
return value
|
|
119
131
|
except (json.JSONDecodeError, UnicodeDecodeError, OSError):
|
|
120
132
|
cache_file.unlink(missing_ok=True)
|
|
121
|
-
|
|
133
|
+
|
|
134
|
+
self._record_cache_metric("miss", "none")
|
|
122
135
|
return None
|
|
123
136
|
|
|
124
|
-
def set(self, key: str, value: Any) -> None:
|
|
137
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
125
138
|
"""Store value in cache.
|
|
126
139
|
|
|
127
140
|
Stores in both memory and file cache for persistence.
|
|
@@ -129,9 +142,12 @@ class CacheManager:
|
|
|
129
142
|
Args:
|
|
130
143
|
key: Cache key.
|
|
131
144
|
value: Value to cache (must be JSON-serializable).
|
|
145
|
+
ttl: Optional time-to-live in seconds. Used as max_age hint
|
|
146
|
+
when retrieving. Stored alongside the value for reference.
|
|
132
147
|
|
|
133
148
|
Example:
|
|
134
149
|
>>> cache.set("search:naruto", results)
|
|
150
|
+
>>> cache.set("mal_id:one_piece", 21, ttl=86400 * 7)
|
|
135
151
|
"""
|
|
136
152
|
self._memory_cache[key] = (value, time.time())
|
|
137
153
|
|
|
@@ -273,7 +273,7 @@ class DependencyManager:
|
|
|
273
273
|
z.extractall(path=temp_extract)
|
|
274
274
|
elif archive_path.endswith((".tar.gz", ".tar.xz", ".tar.bz2", ".tgz")):
|
|
275
275
|
with tarfile.open(archive_path, "r:*") as tar_ref:
|
|
276
|
-
tar_ref.extractall(temp_extract)
|
|
276
|
+
tar_ref.extractall(temp_extract, filter='data')
|
|
277
277
|
else:
|
|
278
278
|
raise Exception("Unsupported format")
|
|
279
279
|
|