Add graceful shutdown, logging, streaming with Range Requests, and state.json cache

- Graceful shutdown: SIGTERM/SIGINT handlers stop continuous scraper,
  disconnect Telegram client, drain running jobs with timeout
- Logging: replace print() with logging module throughout web server,
  basicConfig in main.py with structured log format
- Streaming: chunked file reads (64KB) instead of loading entire files
  into memory; full HTTP Range Request support (206 Partial Content)
  for video seeking in the browser
- Cache: TTL-based in-memory cache (1s) for state.json reads in
  StateStore, thread-safe with existing RLock
- Bugfix: send_error_json now converts HTTPStatus to int for JSON
This commit is contained in:
2026-05-27 00:14:26 +02:00
parent 856d04bd16
commit 1dfe41950e
4 changed files with 171 additions and 20 deletions
+21 -4
View File
@@ -1,7 +1,8 @@
import json
import threading
import time
from pathlib import Path
from typing import Any, Callable, Dict
from typing import Any, Callable, Dict, Optional
DEFAULT_STATE: Dict[str, Any] = {
@@ -24,17 +25,32 @@ class StateStore:
def __init__(self, path: Path):
self.path = path
self.lock = threading.RLock()
self._cache: Optional[Dict[str, Any]] = None
self._cache_time: float = 0
self._cache_ttl: float = 1.0
def load(self) -> Dict[str, Any]:
with self.lock:
now = time.time()
if self._cache is not None and (now - self._cache_time) < self._cache_ttl:
return dict(self._cache)
if not self.path.exists():
return self._default_state()
result = self._default_state()
self._cache = result
self._cache_time = now
return result
try:
with self.path.open("r", encoding="utf-8") as handle:
state = json.load(handle)
except (json.JSONDecodeError, OSError):
return self._default_state()
return self._merge_defaults(state)
result = self._default_state()
self._cache = result
self._cache_time = now
return result
result = self._merge_defaults(state)
self._cache = result
self._cache_time = now
return result
def save(self, state: Dict[str, Any]) -> None:
with self.lock:
@@ -44,6 +60,7 @@ class StateStore:
json.dump(self._merge_defaults(state), handle, ensure_ascii=False, indent=2)
handle.write("\n")
tmp_path.replace(self.path)
self._cache = None
def update(self, mutator: Callable[[Dict[str, Any]], None]) -> Dict[str, Any]:
with self.lock: