weeb-cli 2.9.1__tar.gz → 2.9.4__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.9.1/weeb_cli.egg-info → weeb_cli-2.9.4}/PKG-INFO +3 -4
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/README.md +1 -2
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/pyproject.toml +2 -2
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_cache.py +1 -1
- weeb_cli-2.9.4/tests/test_sanitizer_security.py +52 -0
- weeb_cli-2.9.4/weeb_cli/__init__.py +1 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/api.py +6 -2
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/download_flow.py +37 -2
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/watch_flow.py +9 -8
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/serve.py +6 -8
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_cache.py +15 -15
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_download.py +7 -2
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_menu.py +5 -2
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/config.py +8 -2
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/locales/en.json +26 -1
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/locales/tr.json +26 -1
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/main.py +0 -4
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/animecix.py +10 -2
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/base.py +1 -1
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/cache.py +13 -4
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/database.py +10 -2
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/downloader.py +35 -6
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/tracker.py +78 -70
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/updater.py +1 -1
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/ui/header.py +6 -3
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/ui/menu.py +75 -79
- weeb_cli-2.9.4/weeb_cli/utils/sanitizer.py +81 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4/weeb_cli.egg-info}/PKG-INFO +3 -4
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli.egg-info/SOURCES.txt +1 -0
- weeb_cli-2.9.1/weeb_cli/__init__.py +0 -1
- weeb_cli-2.9.1/weeb_cli/utils/sanitizer.py +0 -45
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/LICENSE +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/setup.cfg +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_anilist_tracker.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_api.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_exceptions.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_kitsu_tracker.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_mal_tracker.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_providers.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_sanitizer.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/__main__.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/downloads.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/library.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/__init__.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/anime_details.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/episode_utils.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/search_handlers.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/stream_utils.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/__init__.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_backup.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_config.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_drives.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_shortcuts.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_trackers.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/setup.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/watchlist.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/exceptions.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/i18n.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/__init__.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/allanime.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/anizle.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/extractors/__init__.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/extractors/megacloud.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/hianime.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/registry.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/turkanime.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/__init__.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/_base.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/_tracker_base.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/dependency_manager.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/details.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/discord_rpc.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/error_handler.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/headless_downloader.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/local_library.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/logger.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/notifier.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/player.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/progress.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/scraper.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/search.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/shortcuts.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/stream_validator.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/watch.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/templates/anilist_error.html +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/templates/anilist_success.html +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/templates/mal_error.html +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/templates/mal_success.html +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/ui/__init__.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/ui/prompt.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/utils/__init__.py +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli.egg-info/dependency_links.txt +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli.egg-info/entry_points.txt +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli.egg-info/requires.txt +0 -0
- {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weeb-cli
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.4
|
|
4
4
|
Summary: Tarayıcı yok, reklam yok, dikkat dağıtıcı unsur yok. Sadece siz ve eşsiz bir anime izleme deneyimi.
|
|
5
5
|
Author-email: ewgsta <ewgst@proton.me>
|
|
6
|
-
License-Expression:
|
|
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
|
|
@@ -53,7 +53,6 @@ Dynamic: license-file
|
|
|
53
53
|
|
|
54
54
|
<p align="center">
|
|
55
55
|
<a href="https://github.com/ewgsta/weeb-cli/releases"><img src="https://img.shields.io/github/v/release/ewgsta/weeb-cli?style=flat-square" alt="Release"></a>
|
|
56
|
-
<a
|
|
57
56
|
<a href="https://github.com/ewgsta/weeb-cli/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0-blue?style=flat-square" alt="License"></a>
|
|
58
57
|
<a href="https://github.com/ewgsta/weeb-cli/stargazers"><img src="https://img.shields.io/github/stars/ewgsta/weeb-cli?style=flat-square" alt="Stars"></a>
|
|
59
58
|
<a href="https://github.com/ewgsta/weeb-cli/actions"><img src="https://img.shields.io/github/actions/workflow/status/ewgsta/weeb-cli/tests.yml?style=flat-square" alt="Tests"></a>
|
|
@@ -392,7 +391,7 @@ weeb-cli/
|
|
|
392
391
|
## Tech Stack
|
|
393
392
|
|
|
394
393
|
### Core Technologies
|
|
395
|
-
- **Python 3.
|
|
394
|
+
- **Python 3.12+** - Main programming language
|
|
396
395
|
- **Typer** - CLI framework with rich terminal support
|
|
397
396
|
- **Rich** - Terminal formatting and styling
|
|
398
397
|
- **Questionary** - Interactive prompts and menus
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://github.com/ewgsta/weeb-cli/releases"><img src="https://img.shields.io/github/v/release/ewgsta/weeb-cli?style=flat-square" alt="Release"></a>
|
|
13
|
-
<a
|
|
14
13
|
<a href="https://github.com/ewgsta/weeb-cli/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0-blue?style=flat-square" alt="License"></a>
|
|
15
14
|
<a href="https://github.com/ewgsta/weeb-cli/stargazers"><img src="https://img.shields.io/github/stars/ewgsta/weeb-cli?style=flat-square" alt="Stars"></a>
|
|
16
15
|
<a href="https://github.com/ewgsta/weeb-cli/actions"><img src="https://img.shields.io/github/actions/workflow/status/ewgsta/weeb-cli/tests.yml?style=flat-square" alt="Tests"></a>
|
|
@@ -349,7 +348,7 @@ weeb-cli/
|
|
|
349
348
|
## Tech Stack
|
|
350
349
|
|
|
351
350
|
### Core Technologies
|
|
352
|
-
- **Python 3.
|
|
351
|
+
- **Python 3.12+** - Main programming language
|
|
353
352
|
- **Typer** - CLI framework with rich terminal support
|
|
354
353
|
- **Rich** - Terminal formatting and styling
|
|
355
354
|
- **Questionary** - Interactive prompts and menus
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weeb-cli"
|
|
7
|
-
version = "2.9.
|
|
7
|
+
version = "2.9.4"
|
|
8
8
|
description = "Tarayıcı yok, reklam yok, dikkat dağıtıcı unsur yok. Sadece siz ve eşsiz bir anime izleme deneyimi."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "ewgsta", email = "ewgst@proton.me" }]
|
|
11
|
-
license = "
|
|
11
|
+
license = "GPL-3.0-only"
|
|
12
12
|
classifiers = [
|
|
13
13
|
"Programming Language :: Python :: 3",
|
|
14
14
|
"Operating System :: OS Independent",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from weeb_cli.utils.sanitizer import sanitize_filename
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestSanitizerSecurity:
|
|
6
|
+
"""Security tests for filename sanitization."""
|
|
7
|
+
|
|
8
|
+
def test_path_traversal_prevention(self):
|
|
9
|
+
"""Test that path traversal attempts are blocked."""
|
|
10
|
+
assert sanitize_filename("../../etc/passwd") == "etcpasswd"
|
|
11
|
+
assert sanitize_filename("..\\..\\windows\\system32") == "windowssystem32"
|
|
12
|
+
assert sanitize_filename("../../../root") == "root"
|
|
13
|
+
|
|
14
|
+
def test_windows_reserved_names(self):
|
|
15
|
+
"""Test Windows reserved filenames are handled."""
|
|
16
|
+
assert sanitize_filename("CON") == "_CON"
|
|
17
|
+
assert sanitize_filename("PRN.txt") == "_PRN.txt"
|
|
18
|
+
assert sanitize_filename("AUX") == "_AUX"
|
|
19
|
+
assert sanitize_filename("NUL.log") == "_NUL.log"
|
|
20
|
+
|
|
21
|
+
def test_invalid_characters_removed(self):
|
|
22
|
+
"""Test that invalid filesystem characters are removed."""
|
|
23
|
+
assert sanitize_filename("file<name>") == "filename"
|
|
24
|
+
assert sanitize_filename('file:name"test') == "filenametest"
|
|
25
|
+
assert sanitize_filename("file|name?") == "filename"
|
|
26
|
+
assert sanitize_filename("file*name") == "filename"
|
|
27
|
+
|
|
28
|
+
def test_control_characters_removed(self):
|
|
29
|
+
"""Test control characters are removed."""
|
|
30
|
+
assert sanitize_filename("file\x00name") == "filename"
|
|
31
|
+
assert sanitize_filename("file\x1fname") == "filename"
|
|
32
|
+
|
|
33
|
+
def test_empty_and_invalid_inputs(self):
|
|
34
|
+
"""Test handling of empty and invalid inputs."""
|
|
35
|
+
assert sanitize_filename("") == "untitled"
|
|
36
|
+
assert sanitize_filename(" ") == "untitled"
|
|
37
|
+
assert sanitize_filename(".") == "untitled"
|
|
38
|
+
assert sanitize_filename("..") == "untitled"
|
|
39
|
+
assert sanitize_filename(None) == "untitled"
|
|
40
|
+
|
|
41
|
+
def test_length_truncation(self):
|
|
42
|
+
"""Test that long filenames are truncated."""
|
|
43
|
+
long_name = "a" * 300
|
|
44
|
+
result = sanitize_filename(long_name)
|
|
45
|
+
assert len(result) <= 200
|
|
46
|
+
|
|
47
|
+
def test_extension_preservation(self):
|
|
48
|
+
"""Test that file extensions are preserved during truncation."""
|
|
49
|
+
long_name = "a" * 300 + ".mp4"
|
|
50
|
+
result = sanitize_filename(long_name)
|
|
51
|
+
assert result.endswith(".mp4")
|
|
52
|
+
assert len(result) <= 200
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.9.4"
|
|
@@ -31,11 +31,15 @@ def _output(data):
|
|
|
31
31
|
|
|
32
32
|
def _quality_score(q: str) -> int:
|
|
33
33
|
q = (q or "").lower()
|
|
34
|
+
if "4k" in q or "2160" in q:
|
|
35
|
+
return 5
|
|
34
36
|
if "1080" in q:
|
|
35
|
-
return
|
|
37
|
+
return 4
|
|
36
38
|
if "720" in q:
|
|
37
|
-
return
|
|
39
|
+
return 3
|
|
38
40
|
if "480" in q:
|
|
41
|
+
return 2
|
|
42
|
+
if "360" in q:
|
|
39
43
|
return 1
|
|
40
44
|
return 0
|
|
41
45
|
|
|
@@ -3,7 +3,7 @@ import questionary
|
|
|
3
3
|
from rich.console import Console
|
|
4
4
|
from weeb_cli.i18n import i18n
|
|
5
5
|
from weeb_cli.services.downloader import queue_manager
|
|
6
|
-
from .episode_utils import get_episodes_safe
|
|
6
|
+
from .episode_utils import get_episodes_safe, group_episodes_by_season
|
|
7
7
|
|
|
8
8
|
console = Console()
|
|
9
9
|
|
|
@@ -14,8 +14,23 @@ def handle_download_flow(slug, details):
|
|
|
14
14
|
time.sleep(1.5)
|
|
15
15
|
return
|
|
16
16
|
|
|
17
|
+
seasons = group_episodes_by_season(episodes)
|
|
18
|
+
season_numbers = sorted(seasons.keys())
|
|
19
|
+
|
|
17
20
|
try:
|
|
18
|
-
|
|
21
|
+
if len(season_numbers) > 1:
|
|
22
|
+
selected_season = _select_season_for_download(seasons, season_numbers)
|
|
23
|
+
if selected_season is None:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
if selected_season == "all":
|
|
27
|
+
target_episodes = episodes
|
|
28
|
+
else:
|
|
29
|
+
target_episodes = seasons[selected_season]
|
|
30
|
+
else:
|
|
31
|
+
target_episodes = episodes
|
|
32
|
+
|
|
33
|
+
selected_eps = _select_episodes_to_download(target_episodes)
|
|
19
34
|
if not selected_eps:
|
|
20
35
|
return
|
|
21
36
|
|
|
@@ -24,6 +39,26 @@ def handle_download_flow(slug, details):
|
|
|
24
39
|
except KeyboardInterrupt:
|
|
25
40
|
return
|
|
26
41
|
|
|
42
|
+
def _select_season_for_download(seasons, season_numbers):
|
|
43
|
+
season_choices = []
|
|
44
|
+
|
|
45
|
+
all_label = i18n.t("details.all_seasons", "Tüm Sezonlar")
|
|
46
|
+
season_choices.append(questionary.Choice(all_label, value="all"))
|
|
47
|
+
|
|
48
|
+
for s_num in season_numbers:
|
|
49
|
+
ep_count = len(seasons[s_num])
|
|
50
|
+
label = f"{i18n.t('details.season', 'Sezon')} {s_num} ({ep_count} {i18n.t('details.episode', 'Bölüm')})"
|
|
51
|
+
season_choices.append(questionary.Choice(label, value=s_num))
|
|
52
|
+
|
|
53
|
+
selected = questionary.select(
|
|
54
|
+
i18n.t("details.select_season", "Sezon Seçin") + ":",
|
|
55
|
+
choices=season_choices,
|
|
56
|
+
pointer=">",
|
|
57
|
+
use_shortcuts=False
|
|
58
|
+
).ask()
|
|
59
|
+
|
|
60
|
+
return selected
|
|
61
|
+
|
|
27
62
|
def _select_episodes_to_download(episodes):
|
|
28
63
|
opt_all = i18n.t("details.download_options.all")
|
|
29
64
|
opt_manual = i18n.t("details.download_options.manual")
|
|
@@ -7,6 +7,7 @@ from weeb_cli.services.watch import get_streams
|
|
|
7
7
|
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
|
+
from weeb_cli.services.logger import error as log_error
|
|
10
11
|
from .episode_utils import get_episodes_safe, group_episodes_by_season, make_season_episode_id
|
|
11
12
|
from .stream_utils import sort_streams, extract_streams_from_response
|
|
12
13
|
|
|
@@ -165,22 +166,21 @@ def _play_episode(slug, selected_ep, details, season, episodes):
|
|
|
165
166
|
return False
|
|
166
167
|
|
|
167
168
|
from weeb_cli.services.stream_validator import stream_validator
|
|
168
|
-
|
|
169
|
-
console.print(f"[dim]{i18n.t('details.validating_streams'
|
|
169
|
+
|
|
170
|
+
console.print(f"[dim]{i18n.t('details.validating_streams')}...[/dim]")
|
|
170
171
|
valid_streams = []
|
|
171
172
|
for stream in streams_list:
|
|
172
173
|
is_valid, error = stream_validator.validate_url(stream.get("url"), timeout=3)
|
|
173
174
|
if is_valid:
|
|
174
175
|
valid_streams.append(stream)
|
|
175
|
-
|
|
176
|
+
|
|
176
177
|
if not valid_streams:
|
|
177
|
-
console.print(f"[red]{i18n.t('details.no_valid_streams'
|
|
178
|
+
console.print(f"[red]{i18n.t('details.no_valid_streams')}[/red]")
|
|
178
179
|
time.sleep(1.5)
|
|
179
180
|
return False
|
|
180
|
-
|
|
181
|
+
|
|
181
182
|
if len(valid_streams) < len(streams_list):
|
|
182
|
-
console.print(f"[dim]{len(valid_streams)}/{len(streams_list)} {i18n.t('details.streams_valid'
|
|
183
|
-
|
|
183
|
+
console.print(f"[dim]{len(valid_streams)}/{len(streams_list)} {i18n.t('details.streams_valid')}[/dim]")
|
|
184
184
|
streams_list = sort_streams(valid_streams)
|
|
185
185
|
|
|
186
186
|
selected_stream = _select_stream(streams_list)
|
|
@@ -249,7 +249,8 @@ def _mark_episode_watched(slug, details, ep_num, season, episodes, completed_ids
|
|
|
249
249
|
|
|
250
250
|
completed_ids.add(season_ep_id)
|
|
251
251
|
return True
|
|
252
|
-
except Exception:
|
|
252
|
+
except Exception as e:
|
|
253
|
+
log_error(f"Failed to mark episode as watched: {e}")
|
|
253
254
|
return False
|
|
254
255
|
|
|
255
256
|
def _update_trackers(details, slug):
|
|
@@ -39,19 +39,17 @@ serve_app = typer.Typer(
|
|
|
39
39
|
|
|
40
40
|
# -- Helpers ------------------------------------------------------------------
|
|
41
41
|
|
|
42
|
-
def _sanitize_for_release(name: str) -> str:
|
|
43
|
-
name = re.sub(r'[<>:"/\\|?*]', "", name)
|
|
44
|
-
name = re.sub(r"[\s]+", ".", name).strip(".")
|
|
45
|
-
return name
|
|
46
|
-
|
|
47
|
-
|
|
48
42
|
def _quality_score(q: str) -> int:
|
|
49
43
|
q = (q or "").lower()
|
|
44
|
+
if "4k" in q or "2160" in q:
|
|
45
|
+
return 5
|
|
50
46
|
if "1080" in q:
|
|
51
|
-
return
|
|
47
|
+
return 4
|
|
52
48
|
if "720" in q:
|
|
53
|
-
return
|
|
49
|
+
return 3
|
|
54
50
|
if "480" in q:
|
|
51
|
+
return 2
|
|
52
|
+
if "360" in q:
|
|
55
53
|
return 1
|
|
56
54
|
return 0
|
|
57
55
|
|
|
@@ -17,24 +17,24 @@ SELECT_STYLE = questionary.Style([
|
|
|
17
17
|
def cache_settings_menu():
|
|
18
18
|
while True:
|
|
19
19
|
console.clear()
|
|
20
|
-
show_header(i18n.t("settings.cache.title"
|
|
20
|
+
show_header(i18n.t("settings.cache.title"))
|
|
21
21
|
|
|
22
22
|
cache = get_cache()
|
|
23
23
|
stats = cache.get_stats()
|
|
24
24
|
|
|
25
|
-
console.print(f"[dim]{i18n.t('settings.cache.memory_entries'
|
|
26
|
-
console.print(f"[dim]{i18n.t('settings.cache.file_entries'
|
|
27
|
-
console.print(f"[dim]{i18n.t('settings.cache.total_size'
|
|
25
|
+
console.print(f"[dim]{i18n.t('settings.cache.memory_entries')}: {stats['memory_entries']}[/dim]")
|
|
26
|
+
console.print(f"[dim]{i18n.t('settings.cache.file_entries')}: {stats['file_entries']}[/dim]")
|
|
27
|
+
console.print(f"[dim]{i18n.t('settings.cache.total_size')}: {stats['total_size_mb']} MB[/dim]\n")
|
|
28
28
|
|
|
29
29
|
choices = [
|
|
30
|
-
i18n.t("settings.cache.clear_all"
|
|
31
|
-
i18n.t("settings.cache.clear_provider"
|
|
32
|
-
i18n.t("settings.cache.cleanup_old"
|
|
30
|
+
i18n.t("settings.cache.clear_all"),
|
|
31
|
+
i18n.t("settings.cache.clear_provider"),
|
|
32
|
+
i18n.t("settings.cache.cleanup_old"),
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
try:
|
|
36
36
|
answer = questionary.select(
|
|
37
|
-
i18n.t("settings.cache.prompt"
|
|
37
|
+
i18n.t("settings.cache.prompt"),
|
|
38
38
|
choices=choices,
|
|
39
39
|
pointer=">",
|
|
40
40
|
use_shortcuts=False,
|
|
@@ -46,24 +46,24 @@ def cache_settings_menu():
|
|
|
46
46
|
if answer is None:
|
|
47
47
|
return
|
|
48
48
|
|
|
49
|
-
if answer == i18n.t("settings.cache.clear_all"
|
|
49
|
+
if answer == i18n.t("settings.cache.clear_all"):
|
|
50
50
|
confirm = questionary.confirm(
|
|
51
|
-
i18n.t("settings.cache.confirm_clear_all"
|
|
51
|
+
i18n.t("settings.cache.confirm_clear_all"),
|
|
52
52
|
default=False
|
|
53
53
|
).ask()
|
|
54
54
|
|
|
55
55
|
if confirm:
|
|
56
56
|
cache.clear()
|
|
57
|
-
console.print(f"[green]{i18n.t('settings.cache.cleared'
|
|
57
|
+
console.print(f"[green]{i18n.t('settings.cache.cleared')}[/green]")
|
|
58
58
|
time.sleep(1)
|
|
59
59
|
|
|
60
|
-
elif answer == i18n.t("settings.cache.clear_provider"
|
|
60
|
+
elif answer == i18n.t("settings.cache.clear_provider"):
|
|
61
61
|
provider = config.get("scraping_source", "None")
|
|
62
62
|
removed = cache.invalidate_provider(provider)
|
|
63
|
-
console.print(f"[green]{i18n.t('settings.cache.provider_cleared'
|
|
63
|
+
console.print(f"[green]{i18n.t('settings.cache.provider_cleared')}: {removed} {i18n.t('common.items')}[/green]")
|
|
64
64
|
time.sleep(1)
|
|
65
65
|
|
|
66
|
-
elif answer == i18n.t("settings.cache.cleanup_old"
|
|
66
|
+
elif answer == i18n.t("settings.cache.cleanup_old"):
|
|
67
67
|
removed = cache.cleanup(max_age=86400)
|
|
68
|
-
console.print(f"[green]{i18n.t('settings.cache.cleaned'
|
|
68
|
+
console.print(f"[green]{i18n.t('settings.cache.cleaned')}: {removed} {i18n.t('common.items')}[/green]")
|
|
69
69
|
time.sleep(1)
|
|
@@ -40,7 +40,7 @@ def download_settings_menu():
|
|
|
40
40
|
show_header(i18n.t("settings.download_settings"))
|
|
41
41
|
|
|
42
42
|
curr_dir = config.get("download_dir")
|
|
43
|
-
console.print(f"[dim]
|
|
43
|
+
console.print(f"[dim]{i18n.t('settings.current_dir', dir=curr_dir)}[/dim]\n", justify="left")
|
|
44
44
|
|
|
45
45
|
curr_concurrent = config.get("max_concurrent_downloads", 3)
|
|
46
46
|
curr_retries = config.get("download_max_retries", 3)
|
|
@@ -106,7 +106,12 @@ def _change_max_retries(curr_retries):
|
|
|
106
106
|
def _change_retry_delay(curr_delay):
|
|
107
107
|
val = questionary.text(i18n.t("settings.enter_retry_delay"), default=str(curr_delay)).ask()
|
|
108
108
|
if val and val.isdigit():
|
|
109
|
-
|
|
109
|
+
n = int(val)
|
|
110
|
+
if 0 <= n <= 300:
|
|
111
|
+
config.set("download_retry_delay", n)
|
|
112
|
+
else:
|
|
113
|
+
console.print(f"[red]{i18n.t('settings.retry_delay_error')}[/red]")
|
|
114
|
+
time.sleep(1.5)
|
|
110
115
|
|
|
111
116
|
def aria2_settings_menu():
|
|
112
117
|
while True:
|
|
@@ -44,8 +44,11 @@ def open_settings():
|
|
|
44
44
|
|
|
45
45
|
def _build_settings_menu():
|
|
46
46
|
lang = config.get("language")
|
|
47
|
-
source = config.get("scraping_source"
|
|
48
|
-
|
|
47
|
+
source = config.get("scraping_source")
|
|
48
|
+
if not source:
|
|
49
|
+
from weeb_cli.providers.registry import get_default_provider
|
|
50
|
+
source = get_default_provider(lang or "tr") or "animecix"
|
|
51
|
+
display_source = source
|
|
49
52
|
|
|
50
53
|
aria2_state = i18n.t("common.enabled") if config.get("aria2_enabled") else i18n.t("common.disabled")
|
|
51
54
|
ytdlp_state = i18n.t("common.enabled") if config.get("ytdlp_enabled") else i18n.t("common.disabled")
|
|
@@ -46,9 +46,15 @@ class Config:
|
|
|
46
46
|
return val
|
|
47
47
|
except Exception:
|
|
48
48
|
pass
|
|
49
|
+
|
|
50
|
+
# Special handling for download_dir
|
|
49
51
|
if key == "download_dir":
|
|
50
|
-
return get_default_download_dir()
|
|
51
|
-
|
|
52
|
+
return default if default is not None else get_default_download_dir()
|
|
53
|
+
|
|
54
|
+
# Use provided default, fallback to DEFAULT_CONFIG, then None
|
|
55
|
+
if default is not None:
|
|
56
|
+
return DEFAULT_CONFIG.get(key, default)
|
|
57
|
+
return DEFAULT_CONFIG.get(key)
|
|
52
58
|
|
|
53
59
|
def set(self, key, value):
|
|
54
60
|
self.db.set_config(key, value)
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"updating_pip": "Updating via pip",
|
|
20
20
|
"no_asset": "Download file not found",
|
|
21
21
|
"download_url": "Download URL",
|
|
22
|
-
"manual_update": "Manual update required"
|
|
22
|
+
"manual_update": "Manual update required",
|
|
23
|
+
"pip_command": "pip install --upgrade weeb-cli"
|
|
23
24
|
},
|
|
24
25
|
"menu": {
|
|
25
26
|
"title": "Main Menu",
|
|
@@ -78,6 +79,22 @@
|
|
|
78
79
|
"confirm_remove": "Are you sure you want to remove this drive?",
|
|
79
80
|
"drive_renamed": "Drive renamed.",
|
|
80
81
|
"drive_removed": "Drive removed.",
|
|
82
|
+
"current_dir": "Current: {dir}",
|
|
83
|
+
"retry_delay_error": "Retry delay must be between 0 and 300 seconds.",
|
|
84
|
+
"cache": {
|
|
85
|
+
"title": "Cache Management",
|
|
86
|
+
"memory_entries": "Memory entries",
|
|
87
|
+
"file_entries": "File entries",
|
|
88
|
+
"total_size": "Total size",
|
|
89
|
+
"clear_all": "Clear all cache",
|
|
90
|
+
"clear_provider": "Clear current provider cache",
|
|
91
|
+
"cleanup_old": "Cleanup old cache (>24h)",
|
|
92
|
+
"cleared": "Cache cleared",
|
|
93
|
+
"provider_cleared": "Provider cache cleared",
|
|
94
|
+
"cleaned": "Cleaned",
|
|
95
|
+
"prompt": "Select action",
|
|
96
|
+
"confirm_clear_all": "Clear all cache?"
|
|
97
|
+
},
|
|
81
98
|
"anilist": "AniList",
|
|
82
99
|
"anilist_connected": "Connected: {user}",
|
|
83
100
|
"anilist_not_connected": "Connect your AniList account to sync your watch history.",
|
|
@@ -229,6 +246,9 @@
|
|
|
229
246
|
"removed_from_library": "Removed from library.",
|
|
230
247
|
"mark_watched": "Mark as watched?",
|
|
231
248
|
"marked_watched": "Marked as watched",
|
|
249
|
+
"validating_streams": "Validating streams...",
|
|
250
|
+
"no_valid_streams": "No valid streams found.",
|
|
251
|
+
"streams_valid": "streams valid",
|
|
232
252
|
"sync_to_trackers": "Sync to trackers as well?",
|
|
233
253
|
"action_prompt": "Select Action",
|
|
234
254
|
"download_options": {
|
|
@@ -285,6 +305,11 @@
|
|
|
285
305
|
"no_streams": "No streams found"
|
|
286
306
|
},
|
|
287
307
|
"downloads": {
|
|
308
|
+
"disk_full": "Insufficient disk space: {free} available. Need at least 1GB free.",
|
|
309
|
+
"no_stream_url": "Stream URL not found",
|
|
310
|
+
"stream_data_failed": "Failed to get stream data",
|
|
311
|
+
"empty_stream_links": "No stream links available",
|
|
312
|
+
"no_valid_streams_found": "No valid streams found after validation",
|
|
288
313
|
"title": "Downloads",
|
|
289
314
|
"empty": "You haven't downloaded anything yet.",
|
|
290
315
|
"status": "Status",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"updating_pip": "pip ile güncelleniyor",
|
|
20
20
|
"no_asset": "İndirme dosyası bulunamadı",
|
|
21
21
|
"download_url": "İndirme URL'si",
|
|
22
|
-
"manual_update": "Manuel güncelleme gerekli"
|
|
22
|
+
"manual_update": "Manuel güncelleme gerekli",
|
|
23
|
+
"pip_command": "pip install --upgrade weeb-cli"
|
|
23
24
|
},
|
|
24
25
|
"menu": {
|
|
25
26
|
"title": "Ana Menü",
|
|
@@ -78,6 +79,22 @@
|
|
|
78
79
|
"confirm_remove": "Bu diski kaldırmak istediğinize emin misiniz?",
|
|
79
80
|
"drive_renamed": "Disk ismi değiştirildi.",
|
|
80
81
|
"drive_removed": "Disk kaldırıldı.",
|
|
82
|
+
"current_dir": "Mevcut: {dir}",
|
|
83
|
+
"retry_delay_error": "Yeniden deneme aralığı 0 ile 300 saniye arasında olmalıdır.",
|
|
84
|
+
"cache": {
|
|
85
|
+
"title": "Önbellek Yönetimi",
|
|
86
|
+
"memory_entries": "Bellek kayıtları",
|
|
87
|
+
"file_entries": "Dosya kayıtları",
|
|
88
|
+
"total_size": "Toplam boyut",
|
|
89
|
+
"clear_all": "Tüm önbelleği temizle",
|
|
90
|
+
"clear_provider": "Mevcut kaynak önbelleğini temizle",
|
|
91
|
+
"cleanup_old": "Eski önbelleği temizle (>24s)",
|
|
92
|
+
"cleared": "Önbellek temizlendi",
|
|
93
|
+
"provider_cleared": "Kaynak önbelleği temizlendi",
|
|
94
|
+
"cleaned": "Temizlendi",
|
|
95
|
+
"prompt": "İşlem seçin",
|
|
96
|
+
"confirm_clear_all": "Tüm önbellek temizlensin mi?"
|
|
97
|
+
},
|
|
81
98
|
"anilist": "AniList",
|
|
82
99
|
"anilist_connected": "Bağlı: {user}",
|
|
83
100
|
"anilist_not_connected": "AniList hesabınıza bağlanarak izleme geçmişinizi senkronize edebilirsiniz.",
|
|
@@ -229,6 +246,9 @@
|
|
|
229
246
|
"removed_from_library": "Kütüphaneden çıkarıldı.",
|
|
230
247
|
"mark_watched": "İzlendi olarak işaretlensin mi?",
|
|
231
248
|
"marked_watched": "İzlendi olarak işaretlendi",
|
|
249
|
+
"validating_streams": "Yayınlar doğrulanıyor...",
|
|
250
|
+
"no_valid_streams": "Geçerli yayın bulunamadı.",
|
|
251
|
+
"streams_valid": "yayın geçerli",
|
|
232
252
|
"sync_to_trackers": "Tracker'lara da eklensin mi?",
|
|
233
253
|
"action_prompt": "İşlem Seçin",
|
|
234
254
|
"download_options": {
|
|
@@ -285,6 +305,11 @@
|
|
|
285
305
|
"no_streams": "Stream bulunamadı"
|
|
286
306
|
},
|
|
287
307
|
"downloads": {
|
|
308
|
+
"disk_full": "Yetersiz disk alanı: {free} mevcut. En az 1GB boş alan gerekli.",
|
|
309
|
+
"no_stream_url": "Stream URL bulunamadı",
|
|
310
|
+
"stream_data_failed": "Stream verisi alınamadı",
|
|
311
|
+
"empty_stream_links": "Stream linkleri bulunamadı",
|
|
312
|
+
"no_valid_streams_found": "Doğrulama sonrası geçerli stream bulunamadı",
|
|
288
313
|
"title": "İndirmeler",
|
|
289
314
|
"empty": "Henüz hiç indirme yapmamışsınız.",
|
|
290
315
|
"status": "Durum",
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
import questionary
|
|
3
3
|
import sys
|
|
4
|
-
import time
|
|
5
4
|
from rich.console import Console
|
|
6
5
|
from weeb_cli.ui.menu import show_main_menu
|
|
7
|
-
from weeb_cli.commands.search import search_anime
|
|
8
|
-
from weeb_cli.commands.watchlist import show_watchlist
|
|
9
|
-
from weeb_cli.commands.settings import open_settings
|
|
10
6
|
from weeb_cli.config import config
|
|
11
7
|
from weeb_cli.i18n import i18n
|
|
12
8
|
from weeb_cli.commands.setup import start_setup_wizard
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import re
|
|
2
3
|
import time
|
|
3
4
|
import urllib.request
|
|
4
5
|
from urllib.parse import urlparse, parse_qs, quote, urlsplit, urlunsplit
|
|
@@ -222,7 +223,15 @@ class AnimeCixProvider(BaseProvider):
|
|
|
222
223
|
quality=label or "auto",
|
|
223
224
|
server="tau-video"
|
|
224
225
|
))
|
|
225
|
-
|
|
226
|
+
|
|
227
|
+
def _quality_sort_key(stream):
|
|
228
|
+
quality = stream.quality.lower().replace('p', '')
|
|
229
|
+
try:
|
|
230
|
+
return -int(quality)
|
|
231
|
+
except ValueError:
|
|
232
|
+
return 1
|
|
233
|
+
|
|
234
|
+
streams.sort(key=_quality_sort_key)
|
|
226
235
|
return streams
|
|
227
236
|
|
|
228
237
|
except Exception:
|
|
@@ -260,7 +269,6 @@ class AnimeCixProvider(BaseProvider):
|
|
|
260
269
|
return "series"
|
|
261
270
|
|
|
262
271
|
def _parse_episode_number(self, name: str, fallback: int) -> int:
|
|
263
|
-
import re
|
|
264
272
|
patterns = [
|
|
265
273
|
r'(?:bölüm|episode|ep)\s*(\d+)',
|
|
266
274
|
r'(\d+)\.\s*(?:bölüm|episode)',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
|
-
from typing import List, Optional, Dict, Any
|
|
3
|
+
from typing import List, Optional, Dict, Any
|
|
4
4
|
from weeb_cli.exceptions import ProviderError
|
|
5
5
|
from weeb_cli.services.logger import debug
|
|
6
6
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import pickle
|
|
2
3
|
import time
|
|
3
4
|
import hashlib
|
|
@@ -30,12 +31,20 @@ class CacheManager:
|
|
|
30
31
|
if cache_file.exists():
|
|
31
32
|
age = time.time() - cache_file.stat().st_mtime
|
|
32
33
|
if age < max_age:
|
|
34
|
+
try:
|
|
35
|
+
with open(cache_file, 'r', encoding='utf-8') as f:
|
|
36
|
+
value = json.load(f)
|
|
37
|
+
self._memory_cache[key] = (value, time.time())
|
|
38
|
+
return value
|
|
39
|
+
except (json.JSONDecodeError, UnicodeDecodeError, OSError):
|
|
40
|
+
pass
|
|
41
|
+
# Fallback: try reading as pickle for backward compatibility with existing cache files
|
|
33
42
|
try:
|
|
34
43
|
with open(cache_file, 'rb') as f:
|
|
35
44
|
value = pickle.load(f)
|
|
36
45
|
self._memory_cache[key] = (value, time.time())
|
|
37
46
|
return value
|
|
38
|
-
except (pickle.PickleError, EOFError):
|
|
47
|
+
except (pickle.PickleError, EOFError, OSError):
|
|
39
48
|
cache_file.unlink(missing_ok=True)
|
|
40
49
|
|
|
41
50
|
return None
|
|
@@ -47,9 +56,9 @@ class CacheManager:
|
|
|
47
56
|
cache_file = self.cache_dir / f"{cache_key}.cache"
|
|
48
57
|
|
|
49
58
|
try:
|
|
50
|
-
with open(cache_file, '
|
|
51
|
-
|
|
52
|
-
except (
|
|
59
|
+
with open(cache_file, 'w', encoding='utf-8') as f:
|
|
60
|
+
json.dump(value, f, ensure_ascii=False, default=str)
|
|
61
|
+
except (OSError, TypeError):
|
|
53
62
|
pass
|
|
54
63
|
|
|
55
64
|
def delete(self, key: str) -> None:
|
|
@@ -29,14 +29,21 @@ class Database:
|
|
|
29
29
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
30
|
self._connection = sqlite3.connect(
|
|
31
31
|
self.db_path,
|
|
32
|
-
timeout=
|
|
32
|
+
timeout=30, # Increased timeout for better reliability
|
|
33
33
|
check_same_thread=False,
|
|
34
34
|
isolation_level=None
|
|
35
35
|
)
|
|
36
36
|
self._connection.row_factory = sqlite3.Row
|
|
37
|
+
# WAL mode for better concurrent access
|
|
37
38
|
self._connection.execute('PRAGMA journal_mode=WAL')
|
|
39
|
+
# NORMAL is safe with WAL mode
|
|
38
40
|
self._connection.execute('PRAGMA synchronous=NORMAL')
|
|
41
|
+
# Larger cache for better performance
|
|
39
42
|
self._connection.execute('PRAGMA cache_size=-64000')
|
|
43
|
+
# Enable foreign keys
|
|
44
|
+
self._connection.execute('PRAGMA foreign_keys=ON')
|
|
45
|
+
# Busy timeout for concurrent access
|
|
46
|
+
self._connection.execute('PRAGMA busy_timeout=30000')
|
|
40
47
|
return self._connection
|
|
41
48
|
|
|
42
49
|
@contextmanager
|
|
@@ -216,7 +223,8 @@ class Database:
|
|
|
216
223
|
if row:
|
|
217
224
|
try:
|
|
218
225
|
return json.loads(row['value'])
|
|
219
|
-
except
|
|
226
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
227
|
+
# If JSON decode fails, return raw value
|
|
220
228
|
return row['value']
|
|
221
229
|
return default
|
|
222
230
|
|