weeb-cli 2.16.3__tar.gz → 3.0.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-3.0.0}/PKG-INFO +1 -1
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/pyproject.toml +1 -1
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_sanitizer.py +2 -2
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_sanitizer_security.py +8 -3
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/__init__.py +1 -1
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/serve_restful.py +36 -15
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/config.py +9 -2
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/i18n.py +7 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/base.py +1 -1
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/de/aniworld.py +6 -1
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/registry.py +21 -17
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/cache.py +4 -1
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/dependency_manager.py +1 -1
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/headless_downloader.py +1 -1
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/notifier.py +24 -13
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/updater.py +5 -3
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/utils/sanitizer.py +2 -3
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli.egg-info/PKG-INFO +1 -1
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/LICENSE +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/README.md +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/setup.cfg +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_anilist_tracker.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_aniworld_provider.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_api.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_cache.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_exceptions.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_kitsu_tracker.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_mal_tracker.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_providers.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_restful_api.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/tests/test_sdk.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/__main__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/api.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/downloads.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/library.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/search/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/search/anime_details.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/search/download_flow.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/search/episode_utils.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/search/search_handlers.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/search/stream_utils.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/search/watch_flow.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/search.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/serve.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/settings/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/settings/settings_backup.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/settings/settings_cache.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/settings/settings_config.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/settings/settings_download.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/settings/settings_drives.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/settings/settings_menu.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/settings/settings_shortcuts.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/settings/settings_trackers.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/settings.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/setup.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/commands/watchlist.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/constants.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/exceptions.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/locales/de.json +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/locales/en.json +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/locales/pl.json +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/locales/tr.json +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/main.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/de/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/en/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/en/allanime.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/en/hianime.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/extractors/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/extractors/doodstream.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/extractors/filemoon.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/extractors/megacloud.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/extractors/streamtape.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/extractors/vidoza.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/extractors/voe.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/pl/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/pl/docchi.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/tr/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/tr/animecix.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/tr/anizle.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/tr/turkanime.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/providers/tr/weeb.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/sdk.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/_base.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/_tracker_base.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/aniskip.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/database.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/details.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/discord_rpc.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/download/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/download/context.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/download/manager.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/download/queue.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/download/strategies/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/download/strategies/aria2.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/download/strategies/base.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/download/strategies/ffmpeg.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/download/strategies/generic.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/download/strategies/ytdlp.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/downloader.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/error_handler.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/local_library.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/logger.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/player.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/progress.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/scraper.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/search.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/shortcuts.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/stream_validator.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/tracker.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/services/watch.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/templates/anilist_error.html +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/templates/anilist_success.html +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/templates/mal_error.html +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/templates/mal_success.html +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/ui/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/ui/header.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/ui/menu.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/ui/prompt.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/utils/__init__.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/utils/decorators.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli/utils/shortcuts.py +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli.egg-info/SOURCES.txt +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli.egg-info/dependency_links.txt +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli.egg-info/entry_points.txt +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli.egg-info/requires.txt +0 -0
- {weeb_cli-2.16.3 → weeb_cli-3.0.0}/weeb_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weeb-cli"
|
|
7
|
-
version = "
|
|
7
|
+
version = "3.0.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" }]
|
|
@@ -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."""
|
|
@@ -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(
|
|
@@ -77,9 +110,6 @@ def serve_restful(
|
|
|
77
110
|
stream=sys.stdout,
|
|
78
111
|
)
|
|
79
112
|
|
|
80
|
-
# Import provider functions
|
|
81
|
-
from weeb_cli.providers.registry import get_provider, list_providers as list_all_providers
|
|
82
|
-
|
|
83
113
|
# Create Flask app
|
|
84
114
|
flask_app = Flask(__name__)
|
|
85
115
|
|
|
@@ -148,10 +178,7 @@ def serve_restful(
|
|
|
148
178
|
|
|
149
179
|
try:
|
|
150
180
|
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
|
-
])
|
|
181
|
+
return jsonify([_serialize_anime_result(r) for r in results])
|
|
155
182
|
except Exception as e:
|
|
156
183
|
log.error(f"Search error: {e}")
|
|
157
184
|
return jsonify({"error": str(e)}), 500
|
|
@@ -189,10 +216,7 @@ def serve_restful(
|
|
|
189
216
|
except ValueError:
|
|
190
217
|
return jsonify({"error": "Invalid season number"}), 400
|
|
191
218
|
|
|
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
|
-
])
|
|
219
|
+
return jsonify([_serialize_episode(e) for e in eps])
|
|
196
220
|
except Exception as e:
|
|
197
221
|
log.error(f"Episodes error: {e}")
|
|
198
222
|
return jsonify({"error": str(e)}), 500
|
|
@@ -238,10 +262,7 @@ def serve_restful(
|
|
|
238
262
|
ep = target[0]
|
|
239
263
|
links = provider.get_streams(anime_id, ep.id)
|
|
240
264
|
|
|
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
|
-
])
|
|
265
|
+
return jsonify([_serialize_stream(s) for s in links])
|
|
245
266
|
except Exception as e:
|
|
246
267
|
log.error(f"Streams error: {e}")
|
|
247
268
|
return jsonify({"error": str(e)}), 500
|
|
@@ -89,6 +89,7 @@ class Config:
|
|
|
89
89
|
"""Initialize configuration manager."""
|
|
90
90
|
self._db: Optional['Database'] = None
|
|
91
91
|
self._headless: bool = False
|
|
92
|
+
self._headless_store: dict = {}
|
|
92
93
|
|
|
93
94
|
@property
|
|
94
95
|
def db(self) -> 'Database':
|
|
@@ -122,13 +123,15 @@ class Config:
|
|
|
122
123
|
>>> config.get("aria2_max_connections")
|
|
123
124
|
16
|
|
124
125
|
"""
|
|
125
|
-
if
|
|
126
|
+
if self._headless:
|
|
127
|
+
if key in self._headless_store:
|
|
128
|
+
return self._headless_store[key]
|
|
129
|
+
else:
|
|
126
130
|
try:
|
|
127
131
|
val = self.db.get_config(key)
|
|
128
132
|
if val is not None:
|
|
129
133
|
return val
|
|
130
134
|
except Exception:
|
|
131
|
-
# Avoid circular import with logger, just pass silently
|
|
132
135
|
pass
|
|
133
136
|
|
|
134
137
|
# Special handling for download_dir
|
|
@@ -144,6 +147,7 @@ class Config:
|
|
|
144
147
|
"""Set configuration value.
|
|
145
148
|
|
|
146
149
|
Persists the value to database for future retrieval.
|
|
150
|
+
In headless mode, stores in-memory only.
|
|
147
151
|
|
|
148
152
|
Args:
|
|
149
153
|
key: Configuration key name.
|
|
@@ -153,6 +157,9 @@ class Config:
|
|
|
153
157
|
>>> config.set("language", "tr")
|
|
154
158
|
>>> config.set("aria2_max_connections", 32)
|
|
155
159
|
"""
|
|
160
|
+
if self._headless:
|
|
161
|
+
self._headless_store[key] = value
|
|
162
|
+
return
|
|
156
163
|
self.db.set_config(key, value)
|
|
157
164
|
|
|
158
165
|
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()
|
|
@@ -273,7 +273,7 @@ class BaseProvider(ABC):
|
|
|
273
273
|
|
|
274
274
|
if attempt < max_retries - 1:
|
|
275
275
|
# Skip retries for permanent errors
|
|
276
|
-
if isinstance(e, requests.HTTPError) and e.response.status_code in [404, 403, 401]:
|
|
276
|
+
if isinstance(e, requests.HTTPError) and hasattr(e, 'response') and e.response is not None and e.response.status_code in [404, 403, 401]:
|
|
277
277
|
debug(f"[HTTP] Permanent error, skipping retries")
|
|
278
278
|
return None
|
|
279
279
|
|
|
@@ -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
|
|
|
@@ -121,7 +121,7 @@ class CacheManager:
|
|
|
121
121
|
|
|
122
122
|
return None
|
|
123
123
|
|
|
124
|
-
def set(self, key: str, value: Any) -> None:
|
|
124
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
125
125
|
"""Store value in cache.
|
|
126
126
|
|
|
127
127
|
Stores in both memory and file cache for persistence.
|
|
@@ -129,9 +129,12 @@ class CacheManager:
|
|
|
129
129
|
Args:
|
|
130
130
|
key: Cache key.
|
|
131
131
|
value: Value to cache (must be JSON-serializable).
|
|
132
|
+
ttl: Optional time-to-live in seconds. Used as max_age hint
|
|
133
|
+
when retrieving. Stored alongside the value for reference.
|
|
132
134
|
|
|
133
135
|
Example:
|
|
134
136
|
>>> cache.set("search:naruto", results)
|
|
137
|
+
>>> cache.set("mal_id:one_piece", 21, ttl=86400 * 7)
|
|
135
138
|
"""
|
|
136
139
|
self._memory_cache[key] = (value, time.time())
|
|
137
140
|
|
|
@@ -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
|
|
|
@@ -55,7 +55,7 @@ def download_episode(
|
|
|
55
55
|
_download_ffmpeg(stream_url, temp_path)
|
|
56
56
|
|
|
57
57
|
if temp_path.exists() and temp_path.stat().st_size > 0:
|
|
58
|
-
|
|
58
|
+
shutil.move(str(temp_path), str(output_path))
|
|
59
59
|
log.info(f"Downloaded: {output_path}")
|
|
60
60
|
return str(output_path)
|
|
61
61
|
else:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import platform
|
|
2
|
+
import re
|
|
2
3
|
import subprocess
|
|
4
|
+
import shutil
|
|
3
5
|
import threading
|
|
4
|
-
from typing import Optional
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def send_notification(title: str, message: str) -> None:
|
|
@@ -26,6 +27,10 @@ def _send_notification_sync(title: str, message: str) -> None:
|
|
|
26
27
|
pass
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
def _sanitize_for_shell(text: str) -> str:
|
|
31
|
+
return re.sub(r'[^\w\s\-.,!?:()\'"/]', '', text)[:256]
|
|
32
|
+
|
|
33
|
+
|
|
29
34
|
def _notify_windows(title: str, message: str) -> None:
|
|
30
35
|
if _try_winotify(title, message):
|
|
31
36
|
return
|
|
@@ -64,15 +69,17 @@ def _try_win10toast(title: str, message: str) -> bool:
|
|
|
64
69
|
|
|
65
70
|
def _try_powershell(title: str, message: str) -> None:
|
|
66
71
|
try:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
safe_title = _sanitize_for_shell(title)
|
|
73
|
+
safe_message = _sanitize_for_shell(message)
|
|
74
|
+
ps_script = (
|
|
75
|
+
'[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null\n'
|
|
76
|
+
'$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)\n'
|
|
77
|
+
'$textNodes = $template.GetElementsByTagName("text")\n'
|
|
78
|
+
f'$textNodes.Item(0).AppendChild($template.CreateTextNode("{safe_title}")) | Out-Null\n'
|
|
79
|
+
f'$textNodes.Item(1).AppendChild($template.CreateTextNode("{safe_message}")) | Out-Null\n'
|
|
80
|
+
'$toast = [Windows.UI.Notifications.ToastNotification]::new($template)\n'
|
|
81
|
+
'[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Weeb CLI").Show($toast)'
|
|
82
|
+
)
|
|
76
83
|
subprocess.run(
|
|
77
84
|
["powershell", "-Command", ps_script],
|
|
78
85
|
capture_output=True,
|
|
@@ -83,10 +90,14 @@ def _try_powershell(title: str, message: str) -> None:
|
|
|
83
90
|
|
|
84
91
|
|
|
85
92
|
def _notify_macos(title: str, message: str) -> None:
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
safe_title = _sanitize_for_shell(title)
|
|
94
|
+
safe_message = _sanitize_for_shell(message)
|
|
95
|
+
script = f'display notification "{safe_message}" with title "{safe_title}"'
|
|
96
|
+
subprocess.run(["osascript", "-e", script], capture_output=True, timeout=5)
|
|
88
97
|
|
|
89
98
|
|
|
90
99
|
def _notify_linux(title: str, message: str) -> None:
|
|
91
|
-
|
|
100
|
+
if not shutil.which("notify-send"):
|
|
101
|
+
return
|
|
102
|
+
subprocess.run(["notify-send", title, message], capture_output=True, timeout=5)
|
|
92
103
|
|
|
@@ -112,11 +112,13 @@ def download_exe(url, filename):
|
|
|
112
112
|
console.print(f"[dim]{i18n.t('update.location')}: {new_exe_path}[/dim]")
|
|
113
113
|
|
|
114
114
|
if getattr(sys, 'frozen', False):
|
|
115
|
+
safe_current = current_exe.replace('^', '^^').replace('&', '^&').replace('|', '^|').replace('<', '^<').replace('>', '^>').replace('%', '%%')
|
|
116
|
+
safe_new = new_exe_path.replace('^', '^^').replace('&', '^&').replace('|', '^|').replace('<', '^<').replace('>', '^>').replace('%', '%%')
|
|
115
117
|
batch_content = f'''@echo off
|
|
116
118
|
timeout /t 2 /nobreak >nul
|
|
117
|
-
del "{
|
|
118
|
-
move "{
|
|
119
|
-
start "" "{
|
|
119
|
+
del "{safe_current}"
|
|
120
|
+
move "{safe_new}" "{safe_current}"
|
|
121
|
+
start "" "{safe_current}"
|
|
120
122
|
del "%~f0"
|
|
121
123
|
'''
|
|
122
124
|
batch_path = os.path.join(download_dir, "update.bat")
|
|
@@ -14,9 +14,8 @@ def sanitize_filename(filename: str, max_length: int = 200) -> str:
|
|
|
14
14
|
filename = filename.replace('..', '')
|
|
15
15
|
filename = Path(filename).name # Extract only filename, remove any path components
|
|
16
16
|
|
|
17
|
-
# Normalize unicode characters
|
|
18
|
-
filename = unicodedata.normalize('
|
|
19
|
-
filename = filename.encode('ascii', 'ignore').decode('ascii')
|
|
17
|
+
# Normalize unicode characters (preserve non-ASCII like Japanese/Turkish)
|
|
18
|
+
filename = unicodedata.normalize('NFKC', filename)
|
|
20
19
|
|
|
21
20
|
# Remove invalid characters for Windows/Linux/macOS
|
|
22
21
|
# Windows: < > : " / \ | ? *
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|