98 lines
3.2 KiB
Python
98 lines
3.2 KiB
Python
"""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
|