weeb-cli 2.5.0__tar.gz → 2.6.1__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 (64) hide show
  1. {weeb_cli-2.5.0/weeb_cli.egg-info → weeb_cli-2.6.1}/PKG-INFO +17 -3
  2. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/README.md +16 -2
  3. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/pyproject.toml +1 -1
  4. weeb_cli-2.6.1/tests/test_cache.py +90 -0
  5. weeb_cli-2.6.1/tests/test_exceptions.py +45 -0
  6. weeb_cli-2.6.1/tests/test_sanitizer.py +69 -0
  7. weeb_cli-2.6.1/weeb_cli/__init__.py +1 -0
  8. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/commands/search.py +12 -2
  9. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/commands/settings.py +0 -3
  10. weeb_cli-2.6.1/weeb_cli/exceptions.py +44 -0
  11. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/providers/allanime.py +47 -4
  12. weeb_cli-2.6.1/weeb_cli/services/cache.py +171 -0
  13. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/database.py +20 -1
  14. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/downloader.py +81 -10
  15. weeb_cli-2.6.1/weeb_cli/services/error_handler.py +0 -0
  16. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/scraper.py +13 -1
  17. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/watch.py +8 -1
  18. weeb_cli-2.6.1/weeb_cli/ui/__init__.py +1 -0
  19. weeb_cli-2.6.1/weeb_cli/utils/__init__.py +4 -0
  20. weeb_cli-2.6.1/weeb_cli/utils/sanitizer.py +79 -0
  21. {weeb_cli-2.5.0 → weeb_cli-2.6.1/weeb_cli.egg-info}/PKG-INFO +17 -3
  22. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli.egg-info/SOURCES.txt +9 -1
  23. weeb_cli-2.5.0/weeb_cli/__init__.py +0 -1
  24. weeb_cli-2.5.0/weeb_cli/ui/__init__.py +0 -1
  25. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/LICENSE +0 -0
  26. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/setup.cfg +0 -0
  27. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/__main__.py +0 -0
  28. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/commands/downloads.py +0 -0
  29. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/commands/setup.py +0 -0
  30. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/commands/watchlist.py +0 -0
  31. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/config.py +0 -0
  32. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/i18n.py +0 -0
  33. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/locales/en.json +0 -0
  34. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/locales/tr.json +0 -0
  35. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/main.py +0 -0
  36. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/providers/__init__.py +0 -0
  37. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/providers/animecix.py +0 -0
  38. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/providers/anizle.py +0 -0
  39. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/providers/base.py +0 -0
  40. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/providers/extractors/__init__.py +0 -0
  41. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/providers/extractors/megacloud.py +0 -0
  42. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/providers/hianime.py +0 -0
  43. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/providers/registry.py +0 -0
  44. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/providers/turkanime.py +0 -0
  45. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/__init__.py +0 -0
  46. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/dependency_manager.py +0 -0
  47. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/details.py +0 -0
  48. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/discord_rpc.py +0 -0
  49. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/local_library.py +0 -0
  50. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/logger.py +0 -0
  51. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/notifier.py +0 -0
  52. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/player.py +0 -0
  53. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/progress.py +0 -0
  54. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/search.py +0 -0
  55. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/shortcuts.py +0 -0
  56. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/tracker.py +0 -0
  57. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/services/updater.py +0 -0
  58. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/ui/header.py +0 -0
  59. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/ui/menu.py +0 -0
  60. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli/ui/prompt.py +0 -0
  61. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli.egg-info/dependency_links.txt +0 -0
  62. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli.egg-info/entry_points.txt +0 -0
  63. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli.egg-info/requires.txt +0 -0
  64. {weeb_cli-2.5.0 → weeb_cli-2.6.1}/weeb_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weeb-cli
3
- Version: 2.5.0
3
+ Version: 2.6.1
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
6
  License-Expression: CC-BY-NC-ND-4.0
@@ -44,6 +44,7 @@ Dynamic: license-file
44
44
  <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>
45
45
  <a href="https://github.com/ewgsta/weeb-cli/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-CC%20BY--NC--ND%204.0-blue?style=flat-square" alt="License"></a>
46
46
  <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>
47
+ <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>
47
48
  </p>
48
49
 
49
50
  <p align="center">
@@ -178,14 +179,27 @@ Yapılandırma: `~/.weeb-cli/weeb.db` (SQLite)
178
179
  - [x] Veritabanı yedekleme/geri yükleme
179
180
  - [x] Klavye kısayolları
180
181
 
181
- ### Planlanan
182
+ ## Gelecek Planlar
183
+
184
+ ### v2.6.0 (Planlanan)
185
+ - [ ] Async/await refactoring
186
+ - [ ] Download strategy pattern
187
+ - [ ] Token şifreleme
188
+ - [ ] Progress bar iyileştirmesi
189
+ - [ ] Plugin sistemi
190
+
191
+ ### v2.7.0 (Planlanan)
182
192
  - [ ] Anime önerileri
183
193
  - [ ] Toplu işlemler
184
194
  - [ ] İzleme istatistikleri (grafik)
185
195
  - [ ] Tema desteği
186
196
  - [ ] Altyazı indirme
187
- - [ ] Torrent desteği (nyaa.si)
197
+
198
+ ### v3.0.0 (Uzun Vadeli)
199
+ - [ ] Web UI (opsiyonel)
200
+ - [ ] Torrent desteği
188
201
  - [ ] Watch party
202
+ - [ ] Mobile app entegrasyonu
189
203
 
190
204
  ---
191
205
 
@@ -12,6 +12,7 @@
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
13
  <a href="https://github.com/ewgsta/weeb-cli/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-CC%20BY--NC--ND%204.0-blue?style=flat-square" alt="License"></a>
14
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>
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>
15
16
  </p>
16
17
 
17
18
  <p align="center">
@@ -146,14 +147,27 @@ Yapılandırma: `~/.weeb-cli/weeb.db` (SQLite)
146
147
  - [x] Veritabanı yedekleme/geri yükleme
147
148
  - [x] Klavye kısayolları
148
149
 
149
- ### Planlanan
150
+ ## Gelecek Planlar
151
+
152
+ ### v2.6.0 (Planlanan)
153
+ - [ ] Async/await refactoring
154
+ - [ ] Download strategy pattern
155
+ - [ ] Token şifreleme
156
+ - [ ] Progress bar iyileştirmesi
157
+ - [ ] Plugin sistemi
158
+
159
+ ### v2.7.0 (Planlanan)
150
160
  - [ ] Anime önerileri
151
161
  - [ ] Toplu işlemler
152
162
  - [ ] İzleme istatistikleri (grafik)
153
163
  - [ ] Tema desteği
154
164
  - [ ] Altyazı indirme
155
- - [ ] Torrent desteği (nyaa.si)
165
+
166
+ ### v3.0.0 (Uzun Vadeli)
167
+ - [ ] Web UI (opsiyonel)
168
+ - [ ] Torrent desteği
156
169
  - [ ] Watch party
170
+ - [ ] Mobile app entegrasyonu
157
171
 
158
172
  ---
159
173
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "weeb-cli"
7
- version = "2.5.0"
7
+ version = "2.6.1"
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" }]
@@ -0,0 +1,90 @@
1
+ """Tests for cache manager."""
2
+ import pytest
3
+ import time
4
+ from weeb_cli.services.cache import CacheManager, cached
5
+
6
+
7
+ class TestCacheManager:
8
+ """Test cache manager functionality."""
9
+
10
+ def test_set_and_get(self, temp_dir):
11
+ """Test basic set and get operations."""
12
+ cache = CacheManager(temp_dir / "cache")
13
+
14
+ cache.set("test_key", "test_value")
15
+ result = cache.get("test_key")
16
+
17
+ assert result == "test_value"
18
+
19
+ def test_expiration(self, temp_dir):
20
+ """Test cache expiration."""
21
+ cache = CacheManager(temp_dir / "cache")
22
+
23
+ cache.set("test_key", "test_value")
24
+
25
+ assert cache.get("test_key", max_age=10) == "test_value"
26
+
27
+ time.sleep(0.1)
28
+ assert cache.get("test_key", max_age=0) is None
29
+
30
+ def test_delete(self, temp_dir):
31
+ """Test cache deletion."""
32
+ cache = CacheManager(temp_dir / "cache")
33
+
34
+ cache.set("test_key", "test_value")
35
+ cache.delete("test_key")
36
+
37
+ assert cache.get("test_key") is None
38
+
39
+ def test_clear(self, temp_dir):
40
+ """Test clearing all cache."""
41
+ cache = CacheManager(temp_dir / "cache")
42
+
43
+ cache.set("key1", "value1")
44
+ cache.set("key2", "value2")
45
+ cache.clear()
46
+
47
+ assert cache.get("key1") is None
48
+ assert cache.get("key2") is None
49
+
50
+ def test_complex_values(self, temp_dir):
51
+ """Test caching complex data structures."""
52
+ cache = CacheManager(temp_dir / "cache")
53
+
54
+ data = {
55
+ "list": [1, 2, 3],
56
+ "dict": {"nested": "value"},
57
+ "tuple": (1, 2, 3)
58
+ }
59
+
60
+ cache.set("complex", data)
61
+ result = cache.get("complex")
62
+
63
+ assert result == data
64
+
65
+
66
+ class TestCachedDecorator:
67
+ """Test cached decorator."""
68
+
69
+ def test_function_caching(self, temp_dir):
70
+ """Test that function results are cached."""
71
+ cache = CacheManager(temp_dir / "cache")
72
+ call_count = 0
73
+
74
+ @cached(max_age=10, cache_manager=cache)
75
+ def expensive_function(x):
76
+ nonlocal call_count
77
+ call_count += 1
78
+ return x * 2
79
+
80
+ result1 = expensive_function(5)
81
+ assert result1 == 10
82
+ assert call_count == 1
83
+
84
+ result2 = expensive_function(5)
85
+ assert result2 == 10
86
+ assert call_count == 1
87
+
88
+ result3 = expensive_function(10)
89
+ assert result3 == 20
90
+ assert call_count == 2
@@ -0,0 +1,45 @@
1
+ """Tests for custom exceptions."""
2
+ import pytest
3
+ from weeb_cli.exceptions import (
4
+ WeebCLIError,
5
+ ProviderError,
6
+ DownloadError,
7
+ NetworkError,
8
+ AuthenticationError
9
+ )
10
+
11
+
12
+ class TestExceptions:
13
+ """Test custom exception hierarchy."""
14
+
15
+ def test_base_exception(self):
16
+ """Test base exception."""
17
+ exc = WeebCLIError("Test message", "TEST_CODE")
18
+ assert str(exc) == "TEST_CODE: Test message"
19
+ assert exc.message == "Test message"
20
+ assert exc.code == "TEST_CODE"
21
+
22
+ def test_exception_without_code(self):
23
+ """Test exception without error code."""
24
+ exc = WeebCLIError("Test message")
25
+ assert str(exc) == "Test message"
26
+
27
+ def test_provider_error(self):
28
+ """Test provider error."""
29
+ exc = ProviderError("Provider failed", "PROVIDER_ERROR")
30
+ assert isinstance(exc, WeebCLIError)
31
+
32
+ def test_download_error(self):
33
+ """Test download error."""
34
+ exc = DownloadError("Download failed", "DOWNLOAD_ERROR")
35
+ assert isinstance(exc, WeebCLIError)
36
+
37
+ def test_network_error(self):
38
+ """Test network error."""
39
+ exc = NetworkError("Network failed", "NETWORK_ERROR")
40
+ assert isinstance(exc, WeebCLIError)
41
+
42
+ def test_authentication_error(self):
43
+ """Test authentication error."""
44
+ exc = AuthenticationError("Auth failed", "AUTH_ERROR")
45
+ assert isinstance(exc, WeebCLIError)
@@ -0,0 +1,69 @@
1
+ """Tests for sanitizer utilities."""
2
+ import pytest
3
+ from weeb_cli.utils.sanitizer import sanitize_filename, validate_url
4
+
5
+
6
+ class TestSanitizeFilename:
7
+ """Test filename sanitization."""
8
+
9
+ def test_basic_sanitization(self):
10
+ """Test basic character removal."""
11
+ assert sanitize_filename("test<>file") == "testfile"
12
+ assert sanitize_filename('test"file') == "testfile"
13
+ assert sanitize_filename("test|file") == "testfile"
14
+
15
+ def test_path_traversal_prevention(self):
16
+ """Test path traversal attack prevention."""
17
+ assert ".." not in sanitize_filename("../../../etc/passwd")
18
+ assert ".." not in sanitize_filename("test..file")
19
+
20
+ def test_empty_input(self):
21
+ """Test empty input handling."""
22
+ assert sanitize_filename("") == "unnamed"
23
+ assert sanitize_filename(" ") == "unnamed"
24
+
25
+ def test_unicode_handling(self):
26
+ """Test unicode character handling."""
27
+ result = sanitize_filename("Anime - 第1話")
28
+ assert result # Should not be empty
29
+ assert len(result) > 0
30
+
31
+ def test_length_limiting(self):
32
+ """Test filename length limiting."""
33
+ long_name = "a" * 300
34
+ result = sanitize_filename(long_name, max_length=200)
35
+ assert len(result) <= 200
36
+
37
+ def test_windows_reserved_chars(self):
38
+ """Test Windows reserved character removal."""
39
+ assert sanitize_filename("file:name") == "filename"
40
+ assert sanitize_filename("file*name") == "filename"
41
+ assert sanitize_filename("file?name") == "filename"
42
+
43
+ def test_leading_trailing_dots(self):
44
+ """Test removal of leading/trailing dots and spaces."""
45
+ assert sanitize_filename("...file...") == "file"
46
+ assert sanitize_filename(" file ") == "file"
47
+
48
+
49
+ class TestValidateUrl:
50
+ """Test URL validation."""
51
+
52
+ def test_valid_urls(self):
53
+ """Test valid URL patterns."""
54
+ assert validate_url("https://example.com")
55
+ assert validate_url("http://example.com")
56
+ assert validate_url("https://example.com/path")
57
+ assert validate_url("https://example.com:8080")
58
+
59
+ def test_invalid_urls(self):
60
+ """Test invalid URL patterns."""
61
+ assert not validate_url("")
62
+ assert not validate_url("not a url")
63
+ assert not validate_url("ftp://example.com")
64
+ assert not validate_url("javascript:alert(1)")
65
+
66
+ def test_localhost(self):
67
+ """Test localhost URLs."""
68
+ assert validate_url("http://localhost")
69
+ assert validate_url("http://localhost:8080")
@@ -0,0 +1 @@
1
+ __version__ = "2.6.1"
@@ -9,6 +9,7 @@ from weeb_cli.services.player import player
9
9
  from weeb_cli.services.progress import progress_tracker
10
10
  from weeb_cli.services.downloader import queue_manager
11
11
  from weeb_cli.services.scraper import scraper
12
+ from weeb_cli.services.cache import get_cache
12
13
  import time
13
14
 
14
15
  console = Console()
@@ -57,8 +58,15 @@ def search_anime():
57
58
 
58
59
  progress_tracker.add_search_history(query.strip())
59
60
 
60
- with console.status(i18n.get("search.searching"), spinner="dots"):
61
- data = search(query)
61
+ cache = get_cache()
62
+ cache_key = f"search:{query.strip()}"
63
+ data = cache.get(cache_key, max_age=1800) # 30 min cache
64
+
65
+ if data is None:
66
+ with console.status(i18n.get("search.searching"), spinner="dots"):
67
+ data = search(query)
68
+ if data:
69
+ cache.set(cache_key, data)
62
70
 
63
71
  if data is None:
64
72
  time.sleep(1)
@@ -160,6 +168,8 @@ def show_anime_details(anime):
160
168
  show_header(details.get("title", ""))
161
169
 
162
170
  if show_desc and desc:
171
+ if len(desc) > 500:
172
+ desc = desc[:497] + "..."
163
173
  console.print(f"\n[dim]{desc}[/dim]\n", justify="left")
164
174
 
165
175
  try:
@@ -174,7 +174,6 @@ def change_language():
174
174
  lang_code = langs[selected]
175
175
  i18n.set_language(lang_code)
176
176
 
177
- # Dil için varsayılan kaynağı ayarla
178
177
  sources = scraper.get_sources_for_lang(lang_code)
179
178
  if sources:
180
179
  config.set("scraping_source", sources[0])
@@ -757,8 +756,6 @@ def restore_backup():
757
756
  except KeyboardInterrupt:
758
757
  pass
759
758
 
760
- # dursun zaman dokunduğunda sana yine yakınlaştığımda bana öyle baktığındaaaaaaa sessizzceeee uyandığında sana yine dokunduğumda dursun zaman
761
-
762
759
 
763
760
  def shortcuts_menu():
764
761
  from weeb_cli.services.shortcuts import shortcut_manager, DEFAULT_SHORTCUTS
@@ -0,0 +1,44 @@
1
+ """Custom exception hierarchy for Weeb CLI."""
2
+
3
+
4
+ class WeebCLIError(Exception):
5
+ """Base exception for all Weeb CLI errors."""
6
+ def __init__(self, message: str = "", code: str = ""):
7
+ self.message = message
8
+ self.code = code
9
+ super().__init__(f"{code}: {message}" if code else message)
10
+
11
+
12
+ class ProviderError(WeebCLIError):
13
+ """Raised when a provider operation fails."""
14
+ pass
15
+
16
+
17
+ class DownloadError(WeebCLIError):
18
+ """Raised when a download operation fails."""
19
+ pass
20
+
21
+
22
+ class NetworkError(WeebCLIError):
23
+ """Raised when a network operation fails."""
24
+ pass
25
+
26
+
27
+ class AuthenticationError(WeebCLIError):
28
+ """Raised when authentication fails."""
29
+ pass
30
+
31
+
32
+ class DatabaseError(WeebCLIError):
33
+ """Raised when a database operation fails."""
34
+ pass
35
+
36
+
37
+ class ValidationError(WeebCLIError):
38
+ """Raised when input validation fails."""
39
+ pass
40
+
41
+
42
+ class DependencyError(WeebCLIError):
43
+ """Raised when a required dependency is missing."""
44
+ pass
@@ -130,23 +130,54 @@ class AllAnimeProvider(BaseProvider):
130
130
 
131
131
  results.append(AnimeResult(
132
132
  id=anime_id,
133
- title=f"{name} ({ep_count} episodes)",
133
+ title=name,
134
134
  type="series"
135
135
  ))
136
136
 
137
137
  return results
138
138
 
139
139
  def get_details(self, anime_id: str) -> Optional[AnimeDetails]:
140
- episodes = self.get_episodes(anime_id)
140
+ gql = '''query ($showId: String!) {
141
+ show(_id: $showId) {
142
+ _id
143
+ name
144
+ description
145
+ thumbnail
146
+ availableEpisodesDetail
147
+ }
148
+ }'''
149
+
150
+ variables = {"showId": anime_id}
151
+ data = _graphql_request(gql, variables)
152
+
153
+ if not data or 'data' not in data:
154
+ return None
141
155
 
142
- if not episodes:
156
+ show = data.get('data', {}).get('show', {})
157
+
158
+ if not show:
143
159
  return None
144
160
 
145
- title = anime_id.replace('-', ' ').title()
161
+ title = show.get('name', anime_id.replace('-', ' ').title())
162
+ description = show.get('description')
163
+ thumbnail = show.get('thumbnail')
164
+
165
+ ep_detail = show.get('availableEpisodesDetail', {})
166
+ ep_list = ep_detail.get(self.mode, [])
167
+
168
+ episodes = []
169
+ for i, ep_num in enumerate(sorted(ep_list, key=lambda x: float(x) if x.replace('.', '').isdigit() else 0)):
170
+ episodes.append(Episode(
171
+ id=f"{anime_id}::ep={ep_num}",
172
+ number=i + 1,
173
+ title=f"Episode {ep_num}"
174
+ ))
146
175
 
147
176
  return AnimeDetails(
148
177
  id=anime_id,
149
178
  title=title,
179
+ description=description,
180
+ cover=thumbnail,
150
181
  episodes=episodes,
151
182
  total_episodes=len(episodes)
152
183
  )
@@ -180,6 +211,10 @@ class AllAnimeProvider(BaseProvider):
180
211
  return episodes
181
212
 
182
213
  def get_streams(self, anime_id: str, episode_id: str) -> List[StreamLink]:
214
+ from weeb_cli.services.logger import debug, error
215
+
216
+ debug(f"[ALLANIME] get_streams: anime_id={anime_id}, episode_id={episode_id}")
217
+
183
218
  if '::ep=' in episode_id:
184
219
  parts = episode_id.split('::ep=')
185
220
  show_id = parts[0]
@@ -188,6 +223,8 @@ class AllAnimeProvider(BaseProvider):
188
223
  show_id = anime_id
189
224
  ep_no = episode_id
190
225
 
226
+ debug(f"[ALLANIME] Parsed: show_id={show_id}, ep_no={ep_no}, mode={self.mode}")
227
+
191
228
  gql = '''query ($showId: String!, $translationType: VaildTranslationTypeEnumType!, $episodeString: String!) {
192
229
  episode(showId: $showId translationType: $translationType episodeString: $episodeString) {
193
230
  episodeString
@@ -201,8 +238,14 @@ class AllAnimeProvider(BaseProvider):
201
238
  "episodeString": ep_no
202
239
  }
203
240
 
241
+ debug(f"[ALLANIME] GraphQL variables: {variables}")
242
+
204
243
  data = _graphql_request(gql, variables)
244
+
245
+ debug(f"[ALLANIME] GraphQL response: {data}")
246
+
205
247
  if not data or 'data' not in data:
248
+ error(f"[ALLANIME] No data in response")
206
249
  return []
207
250
 
208
251
  episode = data.get('data', {}).get('episode', {})
@@ -0,0 +1,171 @@
1
+ """Simple file-based cache manager for Weeb CLI."""
2
+ import pickle
3
+ import time
4
+ import hashlib
5
+ from pathlib import Path
6
+ from typing import Any, Optional, Callable
7
+ from functools import wraps
8
+
9
+
10
+ class CacheManager:
11
+ """Simple file-based cache with TTL support."""
12
+
13
+ def __init__(self, cache_dir: Path):
14
+ """
15
+ Initialize cache manager.
16
+
17
+ Args:
18
+ cache_dir: Directory to store cache files
19
+ """
20
+ self.cache_dir = cache_dir
21
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
22
+ self._memory_cache = {}
23
+
24
+ def _get_cache_key(self, key: str) -> str:
25
+ """Generate a safe cache key from input."""
26
+ return hashlib.md5(key.encode()).hexdigest()
27
+
28
+ def get(self, key: str, max_age: int = 3600) -> Optional[Any]:
29
+ """
30
+ Get value from cache if not expired.
31
+
32
+ Args:
33
+ key: Cache key
34
+ max_age: Maximum age in seconds (default: 1 hour)
35
+
36
+ Returns:
37
+ Cached value or None if expired/not found
38
+ """
39
+ if key in self._memory_cache:
40
+ value, timestamp = self._memory_cache[key]
41
+ if time.time() - timestamp < max_age:
42
+ return value
43
+ else:
44
+ del self._memory_cache[key]
45
+
46
+ cache_key = self._get_cache_key(key)
47
+ cache_file = self.cache_dir / f"{cache_key}.cache"
48
+
49
+ if cache_file.exists():
50
+ age = time.time() - cache_file.stat().st_mtime
51
+ if age < max_age:
52
+ try:
53
+ with open(cache_file, 'rb') as f:
54
+ value = pickle.load(f)
55
+ self._memory_cache[key] = (value, time.time())
56
+ return value
57
+ except (pickle.PickleError, EOFError):
58
+ cache_file.unlink(missing_ok=True)
59
+
60
+ return None
61
+
62
+ def set(self, key: str, value: Any) -> None:
63
+ """
64
+ Store value in cache.
65
+
66
+ Args:
67
+ key: Cache key
68
+ value: Value to cache
69
+ """
70
+ self._memory_cache[key] = (value, time.time())
71
+
72
+ cache_key = self._get_cache_key(key)
73
+ cache_file = self.cache_dir / f"{cache_key}.cache"
74
+
75
+ try:
76
+ with open(cache_file, 'wb') as f:
77
+ pickle.dump(value, f)
78
+ except (pickle.PickleError, OSError):
79
+ pass # Fail silently for cache writes
80
+
81
+ def delete(self, key: str) -> None:
82
+ """
83
+ Delete value from cache.
84
+
85
+ Args:
86
+ key: Cache key
87
+ """
88
+ self._memory_cache.pop(key, None)
89
+
90
+ cache_key = self._get_cache_key(key)
91
+ cache_file = self.cache_dir / f"{cache_key}.cache"
92
+ cache_file.unlink(missing_ok=True)
93
+
94
+ def clear(self) -> None:
95
+ """Clear all cache."""
96
+ self._memory_cache.clear()
97
+
98
+ for cache_file in self.cache_dir.glob("*.cache"):
99
+ cache_file.unlink(missing_ok=True)
100
+
101
+ def cleanup(self, max_age: int = 86400) -> int:
102
+ """
103
+ Remove expired cache files.
104
+
105
+ Args:
106
+ max_age: Maximum age in seconds (default: 24 hours)
107
+
108
+ Returns:
109
+ Number of files removed
110
+ """
111
+ removed = 0
112
+ cutoff = time.time() - max_age
113
+
114
+ for cache_file in self.cache_dir.glob("*.cache"):
115
+ if cache_file.stat().st_mtime < cutoff:
116
+ cache_file.unlink(missing_ok=True)
117
+ removed += 1
118
+
119
+ return removed
120
+
121
+
122
+ def cached(max_age: int = 3600, cache_manager: Optional[CacheManager] = None):
123
+ """
124
+ Decorator to cache function results.
125
+
126
+ Args:
127
+ max_age: Cache TTL in seconds
128
+ cache_manager: CacheManager instance (uses global if None)
129
+
130
+ Example:
131
+ @cached(max_age=1800)
132
+ def expensive_function(arg1, arg2):
133
+ pass
134
+ """
135
+ def decorator(func: Callable) -> Callable:
136
+ @wraps(func)
137
+ def wrapper(*args, **kwargs):
138
+ key_parts = [func.__name__]
139
+ key_parts.extend(str(arg) for arg in args)
140
+ key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
141
+ cache_key = ":".join(key_parts)
142
+
143
+ cm = cache_manager or _get_global_cache()
144
+
145
+ result = cm.get(cache_key, max_age)
146
+ if result is not None:
147
+ return result
148
+
149
+ result = func(*args, **kwargs)
150
+ cm.set(cache_key, result)
151
+ return result
152
+
153
+ return wrapper
154
+ return decorator
155
+
156
+
157
+ _global_cache = None
158
+
159
+
160
+ def _get_global_cache() -> CacheManager:
161
+ """Get or create global cache instance."""
162
+ global _global_cache
163
+ if _global_cache is None:
164
+ from weeb_cli.config import CONFIG_DIR
165
+ _global_cache = CacheManager(CONFIG_DIR / "cache")
166
+ return _global_cache
167
+
168
+
169
+ def get_cache() -> CacheManager:
170
+ """Get global cache instance."""
171
+ return _get_global_cache()
@@ -26,6 +26,8 @@ class Database:
26
26
 
27
27
  def _init_db(self):
28
28
  with self._conn() as conn:
29
+ conn.execute('PRAGMA journal_mode=WAL')
30
+
29
31
  conn.executescript('''
30
32
  CREATE TABLE IF NOT EXISTS config (
31
33
  key TEXT PRIMARY KEY,
@@ -58,7 +60,9 @@ class Database:
58
60
  progress INTEGER DEFAULT 0,
59
61
  eta TEXT DEFAULT '?',
60
62
  error TEXT,
61
- added_at REAL
63
+ added_at REAL,
64
+ retry_count INTEGER DEFAULT 0,
65
+ speed TEXT
62
66
  );
63
67
 
64
68
  CREATE TABLE IF NOT EXISTS external_drives (
@@ -83,6 +87,21 @@ class Database:
83
87
  CREATE INDEX IF NOT EXISTS idx_anime_title ON anime_index(title);
84
88
  CREATE INDEX IF NOT EXISTS idx_anime_source ON anime_index(source_path);
85
89
  ''')
90
+
91
+ self._migrate_columns()
92
+
93
+ def _migrate_columns(self):
94
+ """Add missing columns to existing tables."""
95
+ with self._conn() as conn:
96
+ try:
97
+ conn.execute('SELECT retry_count FROM download_queue LIMIT 1')
98
+ except:
99
+ conn.execute('ALTER TABLE download_queue ADD COLUMN retry_count INTEGER DEFAULT 0')
100
+
101
+ try:
102
+ conn.execute('SELECT speed FROM download_queue LIMIT 1')
103
+ except:
104
+ conn.execute('ALTER TABLE download_queue ADD COLUMN speed TEXT')
86
105
 
87
106
  def _migrate_from_json(self):
88
107
  config_dir = Path.home() / ".weeb-cli"
@@ -7,6 +7,8 @@ from pathlib import Path
7
7
  from rich.console import Console
8
8
  from weeb_cli.config import config
9
9
  from weeb_cli.services.dependency_manager import dependency_manager
10
+ from weeb_cli.utils.sanitizer import sanitize_filename
11
+ from weeb_cli.exceptions import DownloadError
10
12
 
11
13
  console = Console()
12
14
 
@@ -110,7 +112,8 @@ class QueueManager:
110
112
  return added
111
113
 
112
114
  def _sanitize_filename(self, name):
113
- return re.sub(r'[<>:"/\\|?*]', '', name).strip()
115
+ """Sanitize filename for safe file system operations."""
116
+ return sanitize_filename(name)
114
117
 
115
118
  def _manage_queue(self):
116
119
  while self.running:
@@ -203,8 +206,6 @@ class QueueManager:
203
206
  filename = f"{safe_title} - S{season}B{ep_num}.mp4"
204
207
  output_path = anime_dir / filename
205
208
 
206
- debug(f"Getting streams for {item['slug']} - {item['episode_id']}")
207
-
208
209
  stream_data = get_streams(item["slug"], item["episode_id"])
209
210
 
210
211
  if not stream_data:
@@ -212,16 +213,89 @@ class QueueManager:
212
213
  if scraper.last_error:
213
214
  err_msg = f"{err_msg}: {scraper.last_error}"
214
215
  log_error(f"Download failed - {err_msg}")
215
- raise Exception(err_msg)
216
+ raise DownloadError(err_msg, code="NO_STREAM_DATA")
217
+
218
+ if isinstance(stream_data, dict) and "data" in stream_data and "links" in stream_data["data"]:
219
+ links = stream_data["data"]["links"]
220
+ if not links:
221
+ raise DownloadError("Stream links boş", code="EMPTY_STREAM_LINKS")
222
+
223
+ debug(f"Found {len(links)} stream sources, trying each one...")
224
+
225
+ last_error = None
226
+ for idx, link in enumerate(links):
227
+ stream_url = link.get("url")
228
+ server_name = link.get("server", "unknown")
229
+
230
+ if not stream_url:
231
+ continue
232
+
233
+ debug(f"Trying source {idx + 1}/{len(links)}: {server_name} - {stream_url[:80]}...")
234
+
235
+ try:
236
+ self._try_download(stream_url, output_path, item)
237
+ debug(f"Download successful with source: {server_name}")
238
+ return
239
+
240
+ except Exception as e:
241
+ last_error = str(e)
242
+ log_error(f"Source {server_name} failed: {e}")
243
+ if idx < len(links) - 1:
244
+ debug(f"Trying next source...")
245
+ continue
246
+
247
+ raise DownloadError(f"Tüm kaynaklar başarısız. Son hata: {last_error}", code="ALL_SOURCES_FAILED")
216
248
 
217
249
  stream_url = self._extract_url(stream_data)
218
250
 
251
+ debug(f"Extracted stream URL: {stream_url}")
252
+
219
253
  if not stream_url:
220
254
  log_error(f"Download failed - Stream URL bulunamadı. Data: {stream_data}")
221
- raise Exception("Stream URL bulunamadı")
255
+ raise DownloadError("Stream URL bulunamadı", code="NO_STREAM_URL")
222
256
 
223
257
  debug(f"Stream URL found: {stream_url[:80]}...")
224
258
 
259
+ self._try_download(stream_url, output_path, item)
260
+
261
+ def _extract_all_urls(self, data):
262
+ """Extract all available stream URLs with their server names."""
263
+ PRIORITY = ["ALUCARD", "AMATERASU", "SIBNET", "MP4UPLOAD", "UQLOAD"]
264
+
265
+ results = []
266
+
267
+ if isinstance(data, dict):
268
+ node = data
269
+ for _ in range(3):
270
+ if "data" in node and isinstance(node["data"], (dict, list)):
271
+ node = node["data"]
272
+ else:
273
+ break
274
+
275
+ sources = node if isinstance(node, list) else node.get("links") or node.get("sources")
276
+ if sources and isinstance(sources, list) and len(sources) > 0:
277
+ def get_priority(s):
278
+ server = (s.get("server") or "").upper()
279
+ for i, p in enumerate(PRIORITY):
280
+ if p in server:
281
+ return i
282
+ return 999
283
+
284
+ sorted_sources = sorted(sources, key=get_priority)
285
+
286
+ for src in sorted_sources:
287
+ url = src.get("url")
288
+ server = src.get("server", "unknown")
289
+ if url:
290
+ results.append((url, server))
291
+
292
+ elif isinstance(node, dict) and "url" in node:
293
+ results.append((node["url"], node.get("server", "unknown")))
294
+
295
+ return results
296
+
297
+ def _try_download(self, stream_url, output_path, item):
298
+ """Try to download from a stream URL."""
225
299
  is_hls = ".m3u8" in stream_url
226
300
 
227
301
  if is_hls:
@@ -308,7 +382,6 @@ class QueueManager:
308
382
  match = re.search(r'\((\d+)%\)', line)
309
383
  progress = int(match.group(1)) if match else None
310
384
 
311
- # Parse speed (e.g., "DL:1.2MiB")
312
385
  speed = None
313
386
  speed_match = re.search(r'DL:([\d.]+[KMG]?i?B)', line)
314
387
  if speed_match:
@@ -319,7 +392,7 @@ class QueueManager:
319
392
  pass
320
393
 
321
394
  if process.returncode != 0:
322
- raise Exception("Aria2 failed")
395
+ raise DownloadError("Aria2 download failed", code="ARIA2_FAILED")
323
396
 
324
397
  def _download_ytdlp(self, url, path, item):
325
398
  ytdlp = dependency_manager.check_dependency("yt-dlp")
@@ -344,7 +417,6 @@ class QueueManager:
344
417
  progress = float(p_str)
345
418
  eta = line.split("ETA")[-1].strip() if "ETA" in line else None
346
419
 
347
- # Parse speed (e.g., "at 1.5MiB/s")
348
420
  speed = None
349
421
  speed_match = re.search(r'at\s+([\d.]+[KMG]?i?B/s)', line)
350
422
  if speed_match:
@@ -354,7 +426,7 @@ class QueueManager:
354
426
  except:
355
427
  pass
356
428
  if process.returncode != 0:
357
- raise Exception("yt-dlp failed")
429
+ raise DownloadError("yt-dlp download failed", code="YTDLP_FAILED")
358
430
 
359
431
  def _download_ffmpeg(self, url, path, item):
360
432
  self._update_progress(item, eta="")
@@ -390,7 +462,6 @@ class QueueManager:
390
462
  remaining = total - downloaded
391
463
  eta_s = remaining / speed_bytes if speed_bytes > 0 else 0
392
464
 
393
- # Format speed
394
465
  if speed_bytes >= 1024 * 1024:
395
466
  speed_str = f"{speed_bytes / (1024*1024):.1f}MB/s"
396
467
  elif speed_bytes >= 1024:
File without changes
@@ -68,16 +68,28 @@ class Scraper:
68
68
  return []
69
69
 
70
70
  def get_streams(self, anime_id: str, episode_id: str) -> List[StreamLink]:
71
+ from weeb_cli.services.logger import debug, error
72
+
71
73
  self.last_error = None
74
+ debug(f"[SCRAPER] get_streams called: anime_id={anime_id}, episode_id={episode_id}")
75
+ debug(f"[SCRAPER] Current provider: {self._provider_name}")
76
+
72
77
  if not self.provider:
78
+ error("[SCRAPER] No provider available!")
73
79
  return []
80
+
74
81
  try:
75
- return self.provider.get_streams(anime_id, episode_id)
82
+ debug(f"[SCRAPER] Calling provider.get_streams()")
83
+ result = self.provider.get_streams(anime_id, episode_id)
84
+ debug(f"[SCRAPER] Provider returned {len(result) if result else 0} streams")
85
+ return result
76
86
  except ProviderError as e:
77
87
  self.last_error = e.code
88
+ error(f"[SCRAPER] ProviderError: {e.code} - {e.message}")
78
89
  return []
79
90
  except Exception as e:
80
91
  self.last_error = str(e)
92
+ error(f"[SCRAPER] Exception: {str(e)}")
81
93
  return []
82
94
 
83
95
  def get_available_sources(self) -> List[dict]:
@@ -1,11 +1,16 @@
1
1
  from weeb_cli.services.scraper import scraper
2
+ from weeb_cli.services.logger import debug
2
3
 
3
4
  def get_streams(anime_id, episode_id):
5
+ debug(f"[WATCH] Getting streams for {anime_id} - {episode_id}")
4
6
  streams = scraper.get_streams(anime_id, episode_id)
7
+ debug(f"[WATCH] Scraper returned {len(streams) if streams else 0} streams")
8
+
5
9
  if not streams:
10
+ debug(f"[WATCH] No streams found, last_error: {scraper.last_error}")
6
11
  return None
7
12
 
8
- return {
13
+ result = {
9
14
  "data": {
10
15
  "links": [
11
16
  {
@@ -17,3 +22,5 @@ def get_streams(anime_id, episode_id):
17
22
  ]
18
23
  }
19
24
  }
25
+ debug(f"[WATCH] Returning {len(result['data']['links'])} links")
26
+ return result
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,4 @@
1
+ """Utility modules for Weeb CLI."""
2
+ from weeb_cli.utils.sanitizer import sanitize_filename, sanitize_path, validate_url
3
+
4
+ __all__ = ['sanitize_filename', 'sanitize_path', 'validate_url']
@@ -0,0 +1,79 @@
1
+ """Utility functions for sanitizing user input and filenames."""
2
+ import re
3
+ import unicodedata
4
+ from pathlib import Path
5
+
6
+
7
+ def sanitize_filename(name: str, max_length: int = 200) -> str:
8
+ """
9
+ Sanitize a filename to be safe for all operating systems.
10
+
11
+ Args:
12
+ name: The filename to sanitize
13
+ max_length: Maximum length of the filename (default: 200)
14
+
15
+ Returns:
16
+ A safe filename string
17
+ """
18
+ if not name:
19
+ return "unnamed"
20
+
21
+ name = unicodedata.normalize('NFKD', name)
22
+
23
+ name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', '', name)
24
+
25
+ name = name.replace('..', '')
26
+
27
+ name = name.strip('. ')
28
+
29
+ if not name:
30
+ return "unnamed"
31
+
32
+ if len(name) > max_length:
33
+ name = name[:max_length]
34
+
35
+ return name
36
+
37
+
38
+ def sanitize_path(path: str) -> Path:
39
+ """
40
+ Sanitize a full path to prevent directory traversal.
41
+
42
+ Args:
43
+ path: The path to sanitize
44
+
45
+ Returns:
46
+ A safe Path object
47
+ """
48
+ p = Path(path)
49
+
50
+ try:
51
+ resolved = p.resolve()
52
+ return resolved
53
+ except (OSError, RuntimeError):
54
+ raise ValueError(f"Invalid path: {path}")
55
+
56
+
57
+ def validate_url(url: str) -> bool:
58
+ """
59
+ Validate if a URL is safe to use.
60
+
61
+ Args:
62
+ url: The URL to validate
63
+
64
+ Returns:
65
+ True if valid, False otherwise
66
+ """
67
+ if not url:
68
+ return False
69
+
70
+ url_pattern = re.compile(
71
+ r'^https?://'
72
+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|'
73
+ r'localhost|'
74
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
75
+ r'(?::\d+)?'
76
+ r'(?:/?|[/?]\S+)$', re.IGNORECASE
77
+ )
78
+
79
+ return bool(url_pattern.match(url))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weeb-cli
3
- Version: 2.5.0
3
+ Version: 2.6.1
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
6
  License-Expression: CC-BY-NC-ND-4.0
@@ -44,6 +44,7 @@ Dynamic: license-file
44
44
  <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>
45
45
  <a href="https://github.com/ewgsta/weeb-cli/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-CC%20BY--NC--ND%204.0-blue?style=flat-square" alt="License"></a>
46
46
  <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>
47
+ <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>
47
48
  </p>
48
49
 
49
50
  <p align="center">
@@ -178,14 +179,27 @@ Yapılandırma: `~/.weeb-cli/weeb.db` (SQLite)
178
179
  - [x] Veritabanı yedekleme/geri yükleme
179
180
  - [x] Klavye kısayolları
180
181
 
181
- ### Planlanan
182
+ ## Gelecek Planlar
183
+
184
+ ### v2.6.0 (Planlanan)
185
+ - [ ] Async/await refactoring
186
+ - [ ] Download strategy pattern
187
+ - [ ] Token şifreleme
188
+ - [ ] Progress bar iyileştirmesi
189
+ - [ ] Plugin sistemi
190
+
191
+ ### v2.7.0 (Planlanan)
182
192
  - [ ] Anime önerileri
183
193
  - [ ] Toplu işlemler
184
194
  - [ ] İzleme istatistikleri (grafik)
185
195
  - [ ] Tema desteği
186
196
  - [ ] Altyazı indirme
187
- - [ ] Torrent desteği (nyaa.si)
197
+
198
+ ### v3.0.0 (Uzun Vadeli)
199
+ - [ ] Web UI (opsiyonel)
200
+ - [ ] Torrent desteği
188
201
  - [ ] Watch party
202
+ - [ ] Mobile app entegrasyonu
189
203
 
190
204
  ---
191
205
 
@@ -1,9 +1,13 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
+ tests/test_cache.py
5
+ tests/test_exceptions.py
6
+ tests/test_sanitizer.py
4
7
  weeb_cli/__init__.py
5
8
  weeb_cli/__main__.py
6
9
  weeb_cli/config.py
10
+ weeb_cli/exceptions.py
7
11
  weeb_cli/i18n.py
8
12
  weeb_cli/main.py
9
13
  weeb_cli.egg-info/PKG-INFO
@@ -30,11 +34,13 @@ weeb_cli/providers/turkanime.py
30
34
  weeb_cli/providers/extractors/__init__.py
31
35
  weeb_cli/providers/extractors/megacloud.py
32
36
  weeb_cli/services/__init__.py
37
+ weeb_cli/services/cache.py
33
38
  weeb_cli/services/database.py
34
39
  weeb_cli/services/dependency_manager.py
35
40
  weeb_cli/services/details.py
36
41
  weeb_cli/services/discord_rpc.py
37
42
  weeb_cli/services/downloader.py
43
+ weeb_cli/services/error_handler.py
38
44
  weeb_cli/services/local_library.py
39
45
  weeb_cli/services/logger.py
40
46
  weeb_cli/services/notifier.py
@@ -49,4 +55,6 @@ weeb_cli/services/watch.py
49
55
  weeb_cli/ui/__init__.py
50
56
  weeb_cli/ui/header.py
51
57
  weeb_cli/ui/menu.py
52
- weeb_cli/ui/prompt.py
58
+ weeb_cli/ui/prompt.py
59
+ weeb_cli/utils/__init__.py
60
+ weeb_cli/utils/sanitizer.py
@@ -1 +0,0 @@
1
- __version__ = "2.5.0"
@@ -1 +0,0 @@
1
- # Init UI package
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