weeb-cli 2.15.0__tar.gz → 2.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. {weeb_cli-2.15.0/weeb_cli.egg-info → weeb_cli-2.16.0}/PKG-INFO +1 -1
  2. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/pyproject.toml +1 -1
  3. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/__init__.py +1 -1
  4. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/config.py +3 -3
  5. weeb_cli-2.16.0/weeb_cli/constants.py +53 -0
  6. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/registry.py +75 -2
  7. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/database.py +104 -39
  8. weeb_cli-2.16.0/weeb_cli/services/download/__init__.py +10 -0
  9. weeb_cli-2.16.0/weeb_cli/services/download/context.py +21 -0
  10. weeb_cli-2.16.0/weeb_cli/services/download/manager.py +69 -0
  11. weeb_cli-2.16.0/weeb_cli/services/download/queue.py +413 -0
  12. weeb_cli-2.16.0/weeb_cli/services/download/strategies/__init__.py +15 -0
  13. weeb_cli-2.16.0/weeb_cli/services/download/strategies/aria2.py +118 -0
  14. weeb_cli-2.16.0/weeb_cli/services/download/strategies/base.py +45 -0
  15. weeb_cli-2.16.0/weeb_cli/services/download/strategies/ffmpeg.py +91 -0
  16. weeb_cli-2.16.0/weeb_cli/services/download/strategies/generic.py +81 -0
  17. weeb_cli-2.16.0/weeb_cli/services/download/strategies/ytdlp.py +108 -0
  18. weeb_cli-2.16.0/weeb_cli/services/downloader.py +9 -0
  19. weeb_cli-2.16.0/weeb_cli/utils/decorators.py +46 -0
  20. {weeb_cli-2.15.0 → weeb_cli-2.16.0/weeb_cli.egg-info}/PKG-INFO +1 -1
  21. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli.egg-info/SOURCES.txt +12 -0
  22. weeb_cli-2.15.0/weeb_cli/services/downloader.py +0 -588
  23. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/LICENSE +0 -0
  24. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/README.md +0 -0
  25. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/setup.cfg +0 -0
  26. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_anilist_tracker.py +0 -0
  27. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_aniworld_provider.py +0 -0
  28. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_api.py +0 -0
  29. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_cache.py +0 -0
  30. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_exceptions.py +0 -0
  31. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_kitsu_tracker.py +0 -0
  32. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_mal_tracker.py +0 -0
  33. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_providers.py +0 -0
  34. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_restful_api.py +0 -0
  35. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_sanitizer.py +0 -0
  36. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_sanitizer_security.py +0 -0
  37. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_sdk.py +0 -0
  38. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/__main__.py +0 -0
  39. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/__init__.py +0 -0
  40. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/api.py +0 -0
  41. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/downloads.py +0 -0
  42. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/library.py +0 -0
  43. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/__init__.py +0 -0
  44. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/anime_details.py +0 -0
  45. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/download_flow.py +0 -0
  46. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/episode_utils.py +0 -0
  47. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/search_handlers.py +0 -0
  48. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/stream_utils.py +0 -0
  49. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/watch_flow.py +0 -0
  50. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search.py +0 -0
  51. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/serve.py +0 -0
  52. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/serve_restful.py +0 -0
  53. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/__init__.py +0 -0
  54. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_backup.py +0 -0
  55. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_cache.py +0 -0
  56. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_config.py +0 -0
  57. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_download.py +0 -0
  58. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_drives.py +0 -0
  59. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_menu.py +0 -0
  60. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_shortcuts.py +0 -0
  61. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_trackers.py +0 -0
  62. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings.py +0 -0
  63. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/setup.py +0 -0
  64. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/watchlist.py +0 -0
  65. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/exceptions.py +0 -0
  66. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/i18n.py +0 -0
  67. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/locales/de.json +0 -0
  68. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/locales/en.json +0 -0
  69. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/locales/pl.json +0 -0
  70. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/locales/tr.json +0 -0
  71. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/main.py +0 -0
  72. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/__init__.py +0 -0
  73. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/base.py +0 -0
  74. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/de/__init__.py +0 -0
  75. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/de/aniworld.py +0 -0
  76. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/en/__init__.py +0 -0
  77. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/en/allanime.py +0 -0
  78. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/en/hianime.py +0 -0
  79. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/__init__.py +0 -0
  80. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/doodstream.py +0 -0
  81. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/filemoon.py +0 -0
  82. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/megacloud.py +0 -0
  83. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/streamtape.py +0 -0
  84. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/vidoza.py +0 -0
  85. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/voe.py +0 -0
  86. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/pl/__init__.py +0 -0
  87. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/pl/docchi.py +0 -0
  88. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/tr/__init__.py +0 -0
  89. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/tr/animecix.py +0 -0
  90. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/tr/anizle.py +0 -0
  91. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/tr/turkanime.py +0 -0
  92. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/tr/weeb.py +0 -0
  93. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/sdk.py +0 -0
  94. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/__init__.py +0 -0
  95. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/_base.py +0 -0
  96. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/_tracker_base.py +0 -0
  97. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/aniskip.py +0 -0
  98. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/cache.py +0 -0
  99. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/dependency_manager.py +0 -0
  100. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/details.py +0 -0
  101. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/discord_rpc.py +0 -0
  102. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/error_handler.py +0 -0
  103. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/headless_downloader.py +0 -0
  104. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/local_library.py +0 -0
  105. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/logger.py +0 -0
  106. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/notifier.py +0 -0
  107. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/player.py +0 -0
  108. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/progress.py +0 -0
  109. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/scraper.py +0 -0
  110. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/search.py +0 -0
  111. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/shortcuts.py +0 -0
  112. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/stream_validator.py +0 -0
  113. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/tracker.py +0 -0
  114. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/updater.py +0 -0
  115. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/watch.py +0 -0
  116. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/templates/anilist_error.html +0 -0
  117. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/templates/anilist_success.html +0 -0
  118. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/templates/mal_error.html +0 -0
  119. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/templates/mal_success.html +0 -0
  120. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/ui/__init__.py +0 -0
  121. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/ui/header.py +0 -0
  122. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/ui/menu.py +0 -0
  123. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/ui/prompt.py +0 -0
  124. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/utils/__init__.py +0 -0
  125. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/utils/sanitizer.py +0 -0
  126. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli.egg-info/dependency_links.txt +0 -0
  127. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli.egg-info/entry_points.txt +0 -0
  128. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli.egg-info/requires.txt +0 -0
  129. {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weeb-cli
3
- Version: 2.15.0
3
+ Version: 2.16.0
4
4
  Summary: No browser, no ads, no distractions. Just you and an unparalleled anime viewing experience.
5
5
  Author-email: ewgsta <ewgst@proton.me>
6
6
  License-Expression: GPL-3.0-only
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "weeb-cli"
7
- version = "2.15.0"
7
+ version = "2.16.0"
8
8
  description = "No browser, no ads, no distractions. Just you and an unparalleled anime viewing experience."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "ewgsta", email = "ewgst@proton.me" }]
@@ -31,7 +31,7 @@ Attributes:
31
31
  __version__ (str): Current version of Weeb CLI.
32
32
  """
33
33
 
34
- __version__ = "2.15.0"
34
+ __version__ = "2.16.0"
35
35
 
36
36
  # SDK exports for easy import
37
37
  from weeb_cli.sdk import WeebSDK, list_providers, get_provider_info
@@ -127,9 +127,9 @@ class Config:
127
127
  val = self.db.get_config(key)
128
128
  if val is not None:
129
129
  return val
130
- except Exception as e:
131
- from weeb_cli.services.logger import debug
132
- debug(f"[Config] DB read failed for '{key}': {e}")
130
+ except Exception:
131
+ # Avoid circular import with logger, just pass silently
132
+ pass
133
133
 
134
134
  # Special handling for download_dir
135
135
  if key == "download_dir":
@@ -0,0 +1,53 @@
1
+ """Application-wide constants and enumerations.
2
+
3
+ This module defines constants used throughout the application to avoid
4
+ magic strings and improve code maintainability.
5
+ """
6
+
7
+ from enum import Enum
8
+
9
+
10
+ class ConfigKey(str, Enum):
11
+ """Configuration key names."""
12
+
13
+ LANGUAGE = "language"
14
+ ARIA2_ENABLED = "aria2_enabled"
15
+ YTDLP_ENABLED = "ytdlp_enabled"
16
+ ARIA2_MAX_CONNECTIONS = "aria2_max_connections"
17
+ MAX_CONCURRENT_DOWNLOADS = "max_concurrent_downloads"
18
+ DOWNLOAD_DIR = "download_dir"
19
+ YTDLP_FORMAT = "ytdlp_format"
20
+ SCRAPING_SOURCE = "scraping_source"
21
+ SHOW_DESCRIPTION = "show_description"
22
+ DEBUG_MODE = "debug_mode"
23
+ DOWNLOAD_MAX_RETRIES = "download_max_retries"
24
+ DOWNLOAD_RETRY_DELAY = "download_retry_delay"
25
+ DISCORD_RPC_ENABLED = "discord_rpc_enabled"
26
+ SHORTCUTS_ENABLED = "shortcuts_enabled"
27
+ ANISKIP_ENABLED = "aniskip_enabled"
28
+
29
+
30
+ class DownloadStatus(str, Enum):
31
+ """Download queue item status values."""
32
+
33
+ PENDING = "pending"
34
+ PROCESSING = "processing"
35
+ COMPLETED = "completed"
36
+ FAILED = "failed"
37
+ CANCELLED = "cancelled"
38
+
39
+
40
+ class ErrorType(str, Enum):
41
+ """Error classification types."""
42
+
43
+ TRANSIENT = "transient"
44
+ PERMANENT = "permanent"
45
+
46
+
47
+ class Language(str, Enum):
48
+ """Supported language codes."""
49
+
50
+ ENGLISH = "en"
51
+ TURKISH = "tr"
52
+ GERMAN = "de"
53
+ POLISH = "pl"
@@ -40,17 +40,22 @@ Example:
40
40
 
41
41
  import importlib
42
42
  import pkgutil
43
+ import pickle
43
44
  from pathlib import Path
44
45
  from typing import Dict, List, Type, Optional
45
46
 
46
47
  from weeb_cli.providers.base import BaseProvider
47
48
  from weeb_cli.services.logger import debug
49
+ from weeb_cli.config import CONFIG_DIR
48
50
 
49
51
  # Global registry storage
50
52
  _providers: Dict[str, Type[BaseProvider]] = {}
51
53
  _provider_meta: Dict[str, dict] = {}
52
54
  _initialized: bool = False
53
55
 
56
+ # Cache file location
57
+ PROVIDER_CACHE_FILE = CONFIG_DIR / "provider_cache.pkl"
58
+
54
59
 
55
60
  def register_provider(name: str, lang: str = "tr", region: str = "TR", disabled: bool = False):
56
61
  """Decorator for registering provider classes.
@@ -103,11 +108,19 @@ def _discover_providers() -> None:
103
108
 
104
109
  This function is called automatically on first registry access.
105
110
  Uses a global flag to ensure it only runs once.
111
+
112
+ Attempts to load from cache first for faster startup.
106
113
  """
107
- global _initialized
114
+ global _initialized, _providers, _provider_meta
108
115
  if _initialized:
109
116
  return
110
-
117
+
118
+ # Try to load from cache first
119
+ if _load_from_cache():
120
+ _initialized = True
121
+ debug("[Registry] Loaded providers from cache")
122
+ return
123
+
111
124
  base_path = Path(__file__).parent
112
125
 
113
126
  # Scan language directories
@@ -124,9 +137,69 @@ def _discover_providers() -> None:
124
137
  except Exception as e:
125
138
  debug(f"[Registry] Error loading provider {lang}/{name}: {e}")
126
139
 
140
+ # Save to cache for next time
141
+ _save_to_cache()
127
142
  _initialized = True
128
143
 
129
144
 
145
+ def _load_from_cache() -> bool:
146
+ """Load provider registry from cache file.
147
+
148
+ Returns:
149
+ True if cache was loaded successfully, False otherwise.
150
+ """
151
+ global _providers, _provider_meta
152
+
153
+ if not PROVIDER_CACHE_FILE.exists():
154
+ return False
155
+
156
+ try:
157
+ with open(PROVIDER_CACHE_FILE, 'rb') as f:
158
+ cache_data = pickle.load(f)
159
+
160
+ # Validate cache structure
161
+ if not isinstance(cache_data, dict) or 'providers' not in cache_data:
162
+ return False
163
+
164
+ # Import all provider modules to register classes
165
+ for module_path in cache_data.get('modules', []):
166
+ try:
167
+ importlib.import_module(module_path)
168
+ except Exception as e:
169
+ debug(f"[Registry] Failed to import cached module {module_path}: {e}")
170
+ return False
171
+
172
+ return True
173
+ except Exception as e:
174
+ debug(f"[Registry] Failed to load provider cache: {e}")
175
+ return False
176
+
177
+
178
+ def _save_to_cache() -> None:
179
+ """Save provider registry to cache file."""
180
+ try:
181
+ PROVIDER_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
182
+
183
+ # Collect module paths for all registered providers
184
+ modules = set()
185
+ for name, cls in _providers.items():
186
+ module_path = cls.__module__
187
+ modules.add(module_path)
188
+
189
+ cache_data = {
190
+ 'providers': list(_providers.keys()),
191
+ 'modules': list(modules),
192
+ 'meta': _provider_meta
193
+ }
194
+
195
+ with open(PROVIDER_CACHE_FILE, 'wb') as f:
196
+ pickle.dump(cache_data, f)
197
+
198
+ debug(f"[Registry] Saved {len(_providers)} providers to cache")
199
+ except Exception as e:
200
+ debug(f"[Registry] Failed to save provider cache: {e}")
201
+
202
+
130
203
  def get_provider(name: str) -> Optional[BaseProvider]:
131
204
  """Get provider instance by name.
132
205
 
@@ -6,14 +6,101 @@ from pathlib import Path
6
6
  from datetime import datetime
7
7
  from contextlib import contextmanager
8
8
  from typing import Optional, List, Dict, Any
9
+ from queue import Queue, Empty
9
10
 
10
11
  DB_PATH = Path.home() / ".weeb-cli" / "weeb.db"
11
12
 
13
+
14
+ class ConnectionPool:
15
+ """Simple connection pool for SQLite database.
16
+
17
+ Maintains a pool of reusable connections to reduce overhead
18
+ of creating new connections for each operation.
19
+ """
20
+
21
+ def __init__(self, db_path: Path, pool_size: int = 5):
22
+ self.db_path = db_path
23
+ self.pool_size = pool_size
24
+ self._pool: Queue = Queue(maxsize=pool_size)
25
+ self._lock = threading.Lock()
26
+ self._initialized = False
27
+
28
+ def _create_connection(self) -> sqlite3.Connection:
29
+ """Create a new database connection with optimal settings."""
30
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
31
+ conn = sqlite3.connect(
32
+ self.db_path,
33
+ timeout=30,
34
+ check_same_thread=False,
35
+ isolation_level="DEFERRED"
36
+ )
37
+ conn.row_factory = sqlite3.Row
38
+ conn.execute('PRAGMA journal_mode=WAL')
39
+ conn.execute('PRAGMA synchronous=NORMAL')
40
+ conn.execute('PRAGMA cache_size=-64000')
41
+ conn.execute('PRAGMA foreign_keys=ON')
42
+ conn.execute('PRAGMA busy_timeout=30000')
43
+ return conn
44
+
45
+ def _initialize_pool(self):
46
+ """Initialize the connection pool."""
47
+ with self._lock:
48
+ if self._initialized:
49
+ return
50
+ for _ in range(self.pool_size):
51
+ conn = self._create_connection()
52
+ self._pool.put(conn)
53
+ self._initialized = True
54
+
55
+ @contextmanager
56
+ def get_connection(self):
57
+ """Get a connection from the pool.
58
+
59
+ Yields:
60
+ sqlite3.Connection: A database connection.
61
+ """
62
+ if not self._initialized:
63
+ self._initialize_pool()
64
+
65
+ conn = None
66
+ try:
67
+ # Try to get a connection from pool, create new if pool is empty
68
+ try:
69
+ conn = self._pool.get(block=False)
70
+ except Empty:
71
+ conn = self._create_connection()
72
+
73
+ yield conn
74
+ conn.commit()
75
+ except Exception:
76
+ if conn:
77
+ conn.rollback()
78
+ raise
79
+ finally:
80
+ if conn:
81
+ # Try to return to pool, close if pool is full
82
+ try:
83
+ self._pool.put(conn, block=False)
84
+ except:
85
+ conn.close()
86
+
87
+ def close_all(self):
88
+ """Close all connections in the pool."""
89
+ with self._lock:
90
+ while not self._pool.empty():
91
+ try:
92
+ conn = self._pool.get(block=False)
93
+ conn.close()
94
+ except Empty:
95
+ break
96
+ self._initialized = False
97
+
98
+
12
99
  class Database:
13
100
  def __init__(self) -> None:
14
101
  self.db_path: Path = DB_PATH
15
102
  self._initialized: bool = False
16
- self._connection: Optional[sqlite3.Connection] = None
103
+ self._pool: Optional[ConnectionPool] = None
17
104
  self._lock: threading.RLock = threading.RLock()
18
105
 
19
106
  def _ensure_initialized(self) -> None:
@@ -23,40 +110,19 @@ class Database:
23
110
  self._initialized = True
24
111
  self._migrate_from_json()
25
112
 
26
- def _get_connection(self) -> sqlite3.Connection:
113
+ def _get_pool(self) -> ConnectionPool:
114
+ """Get or create the connection pool."""
27
115
  with self._lock:
28
- if self._connection is None:
29
- self.db_path.parent.mkdir(parents=True, exist_ok=True)
30
- self._connection = sqlite3.connect(
31
- self.db_path,
32
- timeout=30, # Increased timeout for better reliability
33
- check_same_thread=False,
34
- isolation_level="DEFERRED" # Removed autocommit for safe transactions
35
- )
36
- self._connection.row_factory = sqlite3.Row
37
- # WAL mode for better concurrent access
38
- self._connection.execute('PRAGMA journal_mode=WAL')
39
- # NORMAL is safe with WAL mode
40
- self._connection.execute('PRAGMA synchronous=NORMAL')
41
- # Larger cache for better performance
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')
47
- return self._connection
116
+ if self._pool is None:
117
+ self._pool = ConnectionPool(self.db_path, pool_size=5)
118
+ return self._pool
48
119
 
49
120
  @contextmanager
50
121
  def _raw_conn(self):
51
122
  self._ensure_initialized()
52
- conn = self._get_connection()
53
- with self._lock:
54
- try:
55
- yield conn
56
- conn.commit()
57
- except Exception:
58
- conn.rollback()
59
- raise
123
+ pool = self._get_pool()
124
+ with pool.get_connection() as conn:
125
+ yield conn
60
126
 
61
127
  @contextmanager
62
128
  def _conn(self):
@@ -64,14 +130,15 @@ class Database:
64
130
  yield conn
65
131
 
66
132
  def close(self) -> None:
133
+ """Close all database connections."""
67
134
  with self._lock:
68
- if self._connection:
69
- self._connection.close()
70
- self._connection = None
135
+ if self._pool:
136
+ self._pool.close_all()
137
+ self._pool = None
71
138
 
72
139
  def _init_db(self):
73
- conn = self._get_connection()
74
- with self._lock:
140
+ pool = self._get_pool()
141
+ with pool.get_connection() as conn:
75
142
  conn.executescript('''
76
143
  CREATE TABLE IF NOT EXISTS config (
77
144
  key TEXT PRIMARY KEY,
@@ -151,8 +218,8 @@ class Database:
151
218
  self._migrate_columns()
152
219
 
153
220
  def _migrate_columns(self):
154
- conn = self._get_connection()
155
- with self._lock:
221
+ pool = self._get_pool()
222
+ with pool.get_connection() as conn:
156
223
  try:
157
224
  conn.execute('SELECT retry_count FROM download_queue LIMIT 1')
158
225
  except sqlite3.OperationalError:
@@ -168,8 +235,6 @@ class Database:
168
235
  except sqlite3.OperationalError:
169
236
  conn.execute('ALTER TABLE progress ADD COLUMN current_time REAL DEFAULT 0')
170
237
  conn.execute('ALTER TABLE progress ADD COLUMN duration REAL DEFAULT 0')
171
-
172
- conn.commit()
173
238
 
174
239
  def _migrate_from_json(self):
175
240
  config_dir = Path.home() / ".weeb-cli"
@@ -0,0 +1,10 @@
1
+ """Download service with strategy-based architecture.
2
+
3
+ This package provides a modular download system with multiple strategies
4
+ for different download methods (Aria2, yt-dlp, FFmpeg, generic HTTP).
5
+ """
6
+
7
+ from weeb_cli.services.download.manager import DownloadManager
8
+ from weeb_cli.services.download.queue import QueueManager, queue_manager
9
+
10
+ __all__ = ['DownloadManager', 'QueueManager', 'queue_manager']
@@ -0,0 +1,21 @@
1
+ """Download context and data structures."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Dict
5
+
6
+
7
+ @dataclass
8
+ class DownloadContext:
9
+ """Context information for a download operation.
10
+
11
+ Attributes:
12
+ url: Stream URL to download.
13
+ output_path: Full path where file should be saved.
14
+ headers: HTTP headers to use for the request.
15
+ item: Queue item metadata (anime title, episode number, etc.).
16
+ """
17
+
18
+ url: str
19
+ output_path: str
20
+ headers: Optional[Dict[str, str]] = None
21
+ item: Optional[dict] = None
@@ -0,0 +1,69 @@
1
+ """Download manager with strategy pattern."""
2
+
3
+ from typing import List
4
+ from pathlib import Path
5
+
6
+ from weeb_cli.services.download.context import DownloadContext
7
+ from weeb_cli.services.download.strategies import (
8
+ DownloadStrategy,
9
+ Aria2Strategy,
10
+ YtdlpStrategy,
11
+ FFmpegStrategy,
12
+ GenericStrategy
13
+ )
14
+ from weeb_cli.services.logger import debug
15
+ from weeb_cli.exceptions import DownloadError
16
+
17
+
18
+ class DownloadManager:
19
+ """Manages download operations using multiple strategies.
20
+
21
+ Tries strategies in priority order until one succeeds.
22
+ """
23
+
24
+ def __init__(self):
25
+ self.strategies: List[DownloadStrategy] = [
26
+ Aria2Strategy(),
27
+ YtdlpStrategy(),
28
+ FFmpegStrategy(),
29
+ GenericStrategy()
30
+ ]
31
+ self.strategies.sort(key=lambda s: s.get_priority())
32
+
33
+ def download(self, url: str, output_path: str, item: dict = None) -> None:
34
+ """Download a file using the best available strategy.
35
+
36
+ Args:
37
+ url: Stream URL to download.
38
+ output_path: Path where file should be saved.
39
+ item: Optional queue item metadata for progress tracking.
40
+
41
+ Raises:
42
+ DownloadError: If all strategies fail.
43
+ """
44
+ context = DownloadContext(
45
+ url=url,
46
+ output_path=output_path,
47
+ item=item
48
+ )
49
+
50
+ last_error = None
51
+
52
+ for strategy in self.strategies:
53
+ if not strategy.can_handle(url):
54
+ continue
55
+
56
+ strategy_name = strategy.__class__.__name__
57
+ debug(f"[DownloadManager] Trying {strategy_name} for {url[:80]}...")
58
+
59
+ try:
60
+ strategy.download(context)
61
+ debug(f"[DownloadManager] Success with {strategy_name}")
62
+ return
63
+ except Exception as e:
64
+ last_error = e
65
+ debug(f"[DownloadManager] {strategy_name} failed: {e}")
66
+ continue
67
+
68
+ error_msg = f"All download strategies failed. Last error: {last_error}"
69
+ raise DownloadError(error_msg, code="ALL_STRATEGIES_FAILED")