diff --git a/tests/test_sanitizer.py b/tests/test_sanitizer.py index 042986f..3c79694 100644 --- a/tests/test_sanitizer.py +++ b/tests/test_sanitizer.py @@ -13,8 +13,8 @@ def test_path_traversal_prevention(self): assert ".." not in sanitize_filename("test..file") def test_empty_input(self): - assert sanitize_filename("") == "unnamed" - assert sanitize_filename(" ") == "unnamed" + assert sanitize_filename("") == "untitled" + assert sanitize_filename(" ") == "untitled" def test_unicode_handling(self): result = sanitize_filename("Anime - 第1話") diff --git a/tests/test_sanitizer_security.py b/tests/test_sanitizer_security.py index 5191eda..bbd7029 100644 --- a/tests/test_sanitizer_security.py +++ b/tests/test_sanitizer_security.py @@ -7,9 +7,14 @@ class TestSanitizerSecurity: def test_path_traversal_prevention(self): """Test that path traversal attempts are blocked.""" - assert sanitize_filename("../../etc/passwd") == "etcpasswd" - assert sanitize_filename("..\\..\\windows\\system32") == "windowssystem32" - assert sanitize_filename("../../../root") == "root" + result = sanitize_filename("../../etc/passwd") + assert ".." not in result + assert "/" not in result + assert "\\" not in result + result2 = sanitize_filename("..\\..\\windows\\system32") + assert ".." not in result2 + result3 = sanitize_filename("../../../root") + assert ".." not in result3 def test_windows_reserved_names(self): """Test Windows reserved filenames are handled.""" diff --git a/weeb_cli/config.py b/weeb_cli/config.py index 937f1a2..c90e200 100644 --- a/weeb_cli/config.py +++ b/weeb_cli/config.py @@ -89,6 +89,7 @@ def __init__(self) -> None: """Initialize configuration manager.""" self._db: Optional['Database'] = None self._headless: bool = False + self._headless_store: dict = {} @property def db(self) -> 'Database': @@ -122,13 +123,15 @@ def get(self, key: str, default: Optional[Any] = None) -> Any: >>> config.get("aria2_max_connections") 16 """ - if not self._headless: + if self._headless: + if key in self._headless_store: + return self._headless_store[key] + else: try: val = self.db.get_config(key) if val is not None: return val except Exception: - # Avoid circular import with logger, just pass silently pass # Special handling for download_dir @@ -144,6 +147,7 @@ def set(self, key: str, value: Any) -> None: """Set configuration value. Persists the value to database for future retrieval. + In headless mode, stores in-memory only. Args: key: Configuration key name. @@ -153,6 +157,9 @@ def set(self, key: str, value: Any) -> None: >>> config.set("language", "tr") >>> config.set("aria2_max_connections", 32) """ + if self._headless: + self._headless_store[key] = value + return self.db.set_config(key, value) def set_headless(self, headless: bool = True) -> None: diff --git a/weeb_cli/providers/base.py b/weeb_cli/providers/base.py index bef23cf..b354e79 100644 --- a/weeb_cli/providers/base.py +++ b/weeb_cli/providers/base.py @@ -273,7 +273,7 @@ def _request( if attempt < max_retries - 1: # Skip retries for permanent errors - if isinstance(e, requests.HTTPError) and e.response.status_code in [404, 403, 401]: + if isinstance(e, requests.HTTPError) and hasattr(e, 'response') and e.response is not None and e.response.status_code in [404, 403, 401]: debug(f"[HTTP] Permanent error, skipping retries") return None diff --git a/weeb_cli/services/cache.py b/weeb_cli/services/cache.py index 356e84a..f408bb4 100644 --- a/weeb_cli/services/cache.py +++ b/weeb_cli/services/cache.py @@ -121,7 +121,7 @@ def get(self, key: str, max_age: int = 3600) -> Optional[Any]: return None - def set(self, key: str, value: Any) -> None: + def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None: """Store value in cache. Stores in both memory and file cache for persistence. @@ -129,9 +129,12 @@ def set(self, key: str, value: Any) -> None: Args: key: Cache key. value: Value to cache (must be JSON-serializable). + ttl: Optional time-to-live in seconds. Used as max_age hint + when retrieving. Stored alongside the value for reference. Example: >>> cache.set("search:naruto", results) + >>> cache.set("mal_id:one_piece", 21, ttl=86400 * 7) """ self._memory_cache[key] = (value, time.time()) diff --git a/weeb_cli/services/headless_downloader.py b/weeb_cli/services/headless_downloader.py index 845e592..f55163f 100644 --- a/weeb_cli/services/headless_downloader.py +++ b/weeb_cli/services/headless_downloader.py @@ -55,7 +55,7 @@ def download_episode( _download_ffmpeg(stream_url, temp_path) if temp_path.exists() and temp_path.stat().st_size > 0: - temp_path.rename(output_path) + shutil.move(str(temp_path), str(output_path)) log.info(f"Downloaded: {output_path}") return str(output_path) else: diff --git a/weeb_cli/utils/sanitizer.py b/weeb_cli/utils/sanitizer.py index a8342fb..3ef2a48 100644 --- a/weeb_cli/utils/sanitizer.py +++ b/weeb_cli/utils/sanitizer.py @@ -14,9 +14,8 @@ def sanitize_filename(filename: str, max_length: int = 200) -> str: filename = filename.replace('..', '') filename = Path(filename).name # Extract only filename, remove any path components - # Normalize unicode characters - filename = unicodedata.normalize('NFKD', filename) - filename = filename.encode('ascii', 'ignore').decode('ascii') + # Normalize unicode characters (preserve non-ASCII like Japanese/Turkish) + filename = unicodedata.normalize('NFKC', filename) # Remove invalid characters for Windows/Linux/macOS # Windows: < > : " / \ | ? *