"""Douban provider implementation. This provider is optional. If no endpoint is configured, it silently degrades. """ from __future__ import annotations import json import time from typing import Optional from urllib.parse import urlencode from urllib.request import urlopen, Request from vlm.providers.base import ProviderResult class DoubanProvider: """Fetch reputation data from a configurable Douban-compatible API.""" name = "douban" def __init__( self, api_key: Optional[str], endpoint: Optional[str] = None, timeout_seconds: int = 6, retries: int = 2, min_interval_seconds: float = 0.4, ) -> None: self.api_key = api_key self.endpoint = endpoint self.timeout_seconds = timeout_seconds self.retries = retries self.min_interval_seconds = min_interval_seconds self._last_request_at = 0.0 def enrich(self, *, title: str, media_type: str, year: Optional[int] = None) -> Optional[ProviderResult]: if not self.endpoint: return None params = { "q": title, "type": media_type, } if year is not None: params["year"] = year if self.api_key: params["api_key"] = self.api_key data = self._get_json(self.endpoint, params) if not data: return None items = data.get("items") or data.get("subjects") or [] if not items: return None item = items[0] score = item.get("rating") or item.get("score") votes = item.get("vote_count") or item.get("ratings_count") title_zh = item.get("title") title_en = item.get("original_title") return ProviderResult( provider=self.name, canonical_id=f"douban:{item.get('id', 'unknown')}", title_zh=title_zh, title_en=title_en, translation_source=self.name if title_zh or title_en else None, reputation_score=float(score) if score is not None else None, reputation_votes=int(votes) if votes is not None else None, reputation_source=self.name, raw_metadata={"id": str(item.get("id", ""))}, ) def _wait_for_rate_limit(self) -> None: if self.min_interval_seconds <= 0: return now = time.monotonic() elapsed = now - self._last_request_at if elapsed < self.min_interval_seconds: time.sleep(self.min_interval_seconds - elapsed) def _get_json(self, url: str, params: dict) -> Optional[dict]: full_url = f"{url}?{urlencode(params)}" request = Request(full_url, headers={"Accept": "application/json"}) for _ in range(max(self.retries + 1, 1)): self._wait_for_rate_limit() try: with urlopen(request, timeout=self.timeout_seconds) as response: payload = response.read().decode("utf-8") self._last_request_at = time.monotonic() return json.loads(payload) except Exception: self._last_request_at = time.monotonic() continue return None