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.
Files changed (97) hide show
  1. {weeb_cli-2.9.1/weeb_cli.egg-info → weeb_cli-2.9.4}/PKG-INFO +3 -4
  2. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/README.md +1 -2
  3. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/pyproject.toml +2 -2
  4. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_cache.py +1 -1
  5. weeb_cli-2.9.4/tests/test_sanitizer_security.py +52 -0
  6. weeb_cli-2.9.4/weeb_cli/__init__.py +1 -0
  7. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/api.py +6 -2
  8. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/download_flow.py +37 -2
  9. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/watch_flow.py +9 -8
  10. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/serve.py +6 -8
  11. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_cache.py +15 -15
  12. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_download.py +7 -2
  13. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_menu.py +5 -2
  14. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/config.py +8 -2
  15. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/locales/en.json +26 -1
  16. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/locales/tr.json +26 -1
  17. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/main.py +0 -4
  18. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/animecix.py +10 -2
  19. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/base.py +1 -1
  20. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/cache.py +13 -4
  21. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/database.py +10 -2
  22. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/downloader.py +35 -6
  23. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/tracker.py +78 -70
  24. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/updater.py +1 -1
  25. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/ui/header.py +6 -3
  26. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/ui/menu.py +75 -79
  27. weeb_cli-2.9.4/weeb_cli/utils/sanitizer.py +81 -0
  28. {weeb_cli-2.9.1 → weeb_cli-2.9.4/weeb_cli.egg-info}/PKG-INFO +3 -4
  29. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli.egg-info/SOURCES.txt +1 -0
  30. weeb_cli-2.9.1/weeb_cli/__init__.py +0 -1
  31. weeb_cli-2.9.1/weeb_cli/utils/sanitizer.py +0 -45
  32. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/LICENSE +0 -0
  33. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/setup.cfg +0 -0
  34. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_anilist_tracker.py +0 -0
  35. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_api.py +0 -0
  36. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_exceptions.py +0 -0
  37. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_kitsu_tracker.py +0 -0
  38. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_mal_tracker.py +0 -0
  39. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_providers.py +0 -0
  40. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/tests/test_sanitizer.py +0 -0
  41. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/__main__.py +0 -0
  42. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/downloads.py +0 -0
  43. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/library.py +0 -0
  44. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/__init__.py +0 -0
  45. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/anime_details.py +0 -0
  46. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/episode_utils.py +0 -0
  47. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/search_handlers.py +0 -0
  48. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search/stream_utils.py +0 -0
  49. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/search.py +0 -0
  50. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/__init__.py +0 -0
  51. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_backup.py +0 -0
  52. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_config.py +0 -0
  53. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_drives.py +0 -0
  54. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_shortcuts.py +0 -0
  55. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings/settings_trackers.py +0 -0
  56. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/settings.py +0 -0
  57. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/setup.py +0 -0
  58. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/commands/watchlist.py +0 -0
  59. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/exceptions.py +0 -0
  60. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/i18n.py +0 -0
  61. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/__init__.py +0 -0
  62. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/allanime.py +0 -0
  63. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/anizle.py +0 -0
  64. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/extractors/__init__.py +0 -0
  65. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/extractors/megacloud.py +0 -0
  66. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/hianime.py +0 -0
  67. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/registry.py +0 -0
  68. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/providers/turkanime.py +0 -0
  69. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/__init__.py +0 -0
  70. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/_base.py +0 -0
  71. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/_tracker_base.py +0 -0
  72. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/dependency_manager.py +0 -0
  73. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/details.py +0 -0
  74. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/discord_rpc.py +0 -0
  75. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/error_handler.py +0 -0
  76. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/headless_downloader.py +0 -0
  77. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/local_library.py +0 -0
  78. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/logger.py +0 -0
  79. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/notifier.py +0 -0
  80. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/player.py +0 -0
  81. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/progress.py +0 -0
  82. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/scraper.py +0 -0
  83. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/search.py +0 -0
  84. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/shortcuts.py +0 -0
  85. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/stream_validator.py +0 -0
  86. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/services/watch.py +0 -0
  87. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/templates/anilist_error.html +0 -0
  88. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/templates/anilist_success.html +0 -0
  89. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/templates/mal_error.html +0 -0
  90. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/templates/mal_success.html +0 -0
  91. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/ui/__init__.py +0 -0
  92. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/ui/prompt.py +0 -0
  93. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli/utils/__init__.py +0 -0
  94. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli.egg-info/dependency_links.txt +0 -0
  95. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli.egg-info/entry_points.txt +0 -0
  96. {weeb_cli-2.9.1 → weeb_cli-2.9.4}/weeb_cli.egg-info/requires.txt +0 -0
  97. {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.1
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: CC-BY-NC-ND-4.0
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.8+** - Main programming language
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.8+** - Main programming language
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.1"
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 = "CC-BY-NC-ND-4.0"
11
+ license = "GPL-3.0-only"
12
12
  classifiers = [
13
13
  "Programming Language :: Python :: 3",
14
14
  "Operating System :: OS Independent",
@@ -46,7 +46,7 @@ class TestCacheManager:
46
46
  data = {
47
47
  "list": [1, 2, 3],
48
48
  "dict": {"nested": "value"},
49
- "tuple": (1, 2, 3)
49
+ "tuple": [1, 2, 3]
50
50
  }
51
51
 
52
52
  cache.set("complex", data)
@@ -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 3
37
+ return 4
36
38
  if "720" in q:
37
- return 2
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
- selected_eps = _select_episodes_to_download(episodes)
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', 'Validating streams')}...[/dim]")
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', 'No valid streams found')}[/red]")
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', 'streams valid')}[/dim]")
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 3
47
+ return 4
52
48
  if "720" in q:
53
- return 2
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", "Cache Management"))
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', 'Memory entries')}: {stats['memory_entries']}[/dim]")
26
- console.print(f"[dim]{i18n.t('settings.cache.file_entries', 'File entries')}: {stats['file_entries']}[/dim]")
27
- console.print(f"[dim]{i18n.t('settings.cache.total_size', 'Total size')}: {stats['total_size_mb']} MB[/dim]\n")
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", "Clear all cache"),
31
- i18n.t("settings.cache.clear_provider", "Clear current provider cache"),
32
- i18n.t("settings.cache.cleanup_old", "Cleanup old cache (>24h)"),
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", "Select action"),
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", "Clear all cache"):
49
+ if answer == i18n.t("settings.cache.clear_all"):
50
50
  confirm = questionary.confirm(
51
- i18n.t("settings.cache.confirm_clear_all", "Clear all cache?"),
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', 'Cache cleared')}[/green]")
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", "Clear current provider cache"):
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', 'Provider cache cleared')}: {removed} {i18n.t('common.items', 'items')}[/green]")
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", "Cleanup old cache (>24h)"):
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', 'Cleaned')}: {removed} {i18n.t('common.items', 'items')}[/green]")
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]Current: {curr_dir}[/dim]\n", justify="left")
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
- config.set("download_retry_delay", int(val))
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", "local")
48
- display_source = "weeb" if source == "local" else source
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
- return DEFAULT_CONFIG.get(key, default)
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, Tuple
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, 'wb') as f:
51
- pickle.dump(value, f)
52
- except (pickle.PickleError, OSError):
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=10,
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 Exception:
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