150 lines
5.4 KiB
Python
150 lines
5.4 KiB
Python
import asyncio
|
|
|
|
from server.services.covers import covers
|
|
from server.services.device import DeviceProfile
|
|
from server.services.http import http
|
|
from server.services.rapt import API_BASE
|
|
from server.services.sessions import sessions
|
|
|
|
CLASSIFY_MAX_PAGES = 50
|
|
RECOMMEND_MAX_PAGES = 80
|
|
FIXED_SECTIONS = (
|
|
{"key": "new_releases", "name": "New Releases", "note": "", "loading": True},
|
|
{"key": "recommend", "name": "Recommend", "note": "", "loading": True},
|
|
)
|
|
|
|
|
|
class CatalogService:
|
|
def outlines(self, categories: list[dict]) -> list[dict]:
|
|
rows = [
|
|
{"key": f"type-{item['id']}", "name": item["label"], "note": "", "loading": True}
|
|
for item in categories
|
|
]
|
|
rows.extend(dict(item) for item in FIXED_SECTIONS)
|
|
return rows
|
|
|
|
def _categories(self, token: str, device: DeviceProfile) -> list[dict]:
|
|
data = http.request_data("POST", API_BASE + "/app/open/classifyv2", token, device, {})
|
|
rows = []
|
|
if isinstance(data, list):
|
|
for item in data:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
type_id = item.get("id")
|
|
label = str(item.get("label") or "").strip()
|
|
if type_id is None or not label:
|
|
continue
|
|
rows.append({"id": type_id, "label": label})
|
|
if not rows:
|
|
raise RuntimeError("没有获取到分类")
|
|
return rows
|
|
|
|
async def stream(self, token: str, raw_device: dict | None):
|
|
token = token.strip()
|
|
if not token:
|
|
raise RuntimeError("请先登录")
|
|
device = sessions.bind(token, DeviceProfile.merge(raw_device))
|
|
categories = await asyncio.to_thread(self._categories, token, device)
|
|
yield {"type": "outline", "sections": self.outlines(categories)}
|
|
jobs = [
|
|
(
|
|
f"type-{item['id']}",
|
|
item["label"],
|
|
"/app/open/classify_video",
|
|
"POST",
|
|
lambda page, type_id=item["id"]: {"page": page, "limit": 10, "type": type_id},
|
|
CLASSIFY_MAX_PAGES,
|
|
"",
|
|
)
|
|
for item in categories
|
|
]
|
|
jobs.append(("new_releases", "New Releases", "/app/open/orgin", "GET", None, 1, ""))
|
|
jobs.append((
|
|
"recommend",
|
|
"Recommend",
|
|
"/app/open/recommend",
|
|
"POST",
|
|
lambda page: {"page": page, "limit": 10},
|
|
RECOMMEND_MAX_PAGES,
|
|
"",
|
|
))
|
|
loop = asyncio.get_running_loop()
|
|
queue: asyncio.Queue = asyncio.Queue()
|
|
|
|
async def run(job):
|
|
key, name, path, method, body, max_pages, note = job
|
|
|
|
def work():
|
|
try:
|
|
for event in self._iter_paged(key, name, token, device, path, method, body, max_pages, note):
|
|
loop.call_soon_threadsafe(queue.put_nowait, event)
|
|
except Exception as exc:
|
|
loop.call_soon_threadsafe(queue.put_nowait, self._event(key, name, [], str(exc) or "获取清单失败", True))
|
|
finally:
|
|
loop.call_soon_threadsafe(queue.put_nowait, None)
|
|
|
|
await asyncio.to_thread(work)
|
|
|
|
tasks = [asyncio.create_task(run(job)) for job in jobs]
|
|
finished = 0
|
|
try:
|
|
while finished < len(tasks):
|
|
event = await queue.get()
|
|
if event is None:
|
|
finished += 1
|
|
continue
|
|
yield event
|
|
yield {"type": "done"}
|
|
finally:
|
|
for task in tasks:
|
|
if not task.done():
|
|
task.cancel()
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
def _event(self, key: str, name: str, items: list[dict], note: str = "", done: bool = False) -> dict:
|
|
return {"type": "page", "key": key, "name": name, "note": note, "items": items, "done": done}
|
|
|
|
def _drama(self, item: dict) -> dict | None:
|
|
vid = item.get("id")
|
|
title = item.get("title")
|
|
if vid is None or not title:
|
|
return None
|
|
image = (item.get("image") or "").strip()
|
|
return {
|
|
"id": vid,
|
|
"title": title,
|
|
"image": covers.local_url(image) if image else "",
|
|
}
|
|
|
|
def _iter_paged(self, key: str, name: str, token: str, device: DeviceProfile, path: str, method: str, body, max_pages: int, note: str = ""):
|
|
seen = set()
|
|
page = 1
|
|
sent = False
|
|
while page <= max_pages:
|
|
data = http.request_data(method, API_BASE + path, token, device, body(page) if body else None)
|
|
batch = data if isinstance(data, list) else []
|
|
if not batch and isinstance(data, dict) and isinstance(data.get("list"), list):
|
|
batch = data["list"]
|
|
if not batch:
|
|
break
|
|
fresh = []
|
|
for item in batch:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
drama = self._drama(item)
|
|
if drama is None or drama["id"] in seen:
|
|
continue
|
|
seen.add(drama["id"])
|
|
fresh.append(drama)
|
|
if not fresh:
|
|
break
|
|
sent = True
|
|
yield self._event(key, name, fresh)
|
|
if body is None:
|
|
break
|
|
page += 1
|
|
yield self._event(key, name, [], note, True)
|
|
|
|
|
|
catalog = CatalogService()
|