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.
- {weeb_cli-2.15.0/weeb_cli.egg-info → weeb_cli-2.16.0}/PKG-INFO +1 -1
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/pyproject.toml +1 -1
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/__init__.py +1 -1
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/config.py +3 -3
- weeb_cli-2.16.0/weeb_cli/constants.py +53 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/registry.py +75 -2
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/database.py +104 -39
- weeb_cli-2.16.0/weeb_cli/services/download/__init__.py +10 -0
- weeb_cli-2.16.0/weeb_cli/services/download/context.py +21 -0
- weeb_cli-2.16.0/weeb_cli/services/download/manager.py +69 -0
- weeb_cli-2.16.0/weeb_cli/services/download/queue.py +413 -0
- weeb_cli-2.16.0/weeb_cli/services/download/strategies/__init__.py +15 -0
- weeb_cli-2.16.0/weeb_cli/services/download/strategies/aria2.py +118 -0
- weeb_cli-2.16.0/weeb_cli/services/download/strategies/base.py +45 -0
- weeb_cli-2.16.0/weeb_cli/services/download/strategies/ffmpeg.py +91 -0
- weeb_cli-2.16.0/weeb_cli/services/download/strategies/generic.py +81 -0
- weeb_cli-2.16.0/weeb_cli/services/download/strategies/ytdlp.py +108 -0
- weeb_cli-2.16.0/weeb_cli/services/downloader.py +9 -0
- weeb_cli-2.16.0/weeb_cli/utils/decorators.py +46 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0/weeb_cli.egg-info}/PKG-INFO +1 -1
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli.egg-info/SOURCES.txt +12 -0
- weeb_cli-2.15.0/weeb_cli/services/downloader.py +0 -588
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/LICENSE +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/README.md +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/setup.cfg +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_anilist_tracker.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_aniworld_provider.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_api.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_cache.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_exceptions.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_kitsu_tracker.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_mal_tracker.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_providers.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_restful_api.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_sanitizer.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_sanitizer_security.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/tests/test_sdk.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/__main__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/api.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/downloads.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/library.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/anime_details.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/download_flow.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/episode_utils.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/search_handlers.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/stream_utils.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search/watch_flow.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/search.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/serve.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/serve_restful.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_backup.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_cache.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_config.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_download.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_drives.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_menu.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_shortcuts.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings/settings_trackers.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/settings.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/setup.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/commands/watchlist.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/exceptions.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/i18n.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/locales/de.json +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/locales/en.json +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/locales/pl.json +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/locales/tr.json +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/main.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/base.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/de/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/de/aniworld.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/en/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/en/allanime.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/en/hianime.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/doodstream.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/filemoon.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/megacloud.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/streamtape.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/vidoza.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/extractors/voe.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/pl/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/pl/docchi.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/tr/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/tr/animecix.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/tr/anizle.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/tr/turkanime.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/providers/tr/weeb.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/sdk.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/_base.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/_tracker_base.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/aniskip.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/cache.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/dependency_manager.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/details.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/discord_rpc.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/error_handler.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/headless_downloader.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/local_library.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/logger.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/notifier.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/player.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/progress.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/scraper.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/search.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/shortcuts.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/stream_validator.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/tracker.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/updater.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/services/watch.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/templates/anilist_error.html +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/templates/anilist_success.html +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/templates/mal_error.html +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/templates/mal_success.html +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/ui/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/ui/header.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/ui/menu.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/ui/prompt.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/utils/__init__.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli/utils/sanitizer.py +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli.egg-info/dependency_links.txt +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli.egg-info/entry_points.txt +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli.egg-info/requires.txt +0 -0
- {weeb_cli-2.15.0 → weeb_cli-2.16.0}/weeb_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weeb-cli"
|
|
7
|
-
version = "2.
|
|
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" }]
|
|
@@ -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
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
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
|
|
113
|
+
def _get_pool(self) -> ConnectionPool:
|
|
114
|
+
"""Get or create the connection pool."""
|
|
27
115
|
with self._lock:
|
|
28
|
-
if self.
|
|
29
|
-
self.db_path
|
|
30
|
-
|
|
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
|
-
|
|
53
|
-
with
|
|
54
|
-
|
|
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.
|
|
69
|
-
self.
|
|
70
|
-
self.
|
|
135
|
+
if self._pool:
|
|
136
|
+
self._pool.close_all()
|
|
137
|
+
self._pool = None
|
|
71
138
|
|
|
72
139
|
def _init_db(self):
|
|
73
|
-
|
|
74
|
-
with
|
|
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
|
-
|
|
155
|
-
with
|
|
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")
|