Files
re-xxm/reaction_board/bilibili/models.py
T
kairee 5c2ab7b187 feat(p1): add daily snapshots and fan tracking
Implement P1: seven-metric view snapshots, relation/stat fan snapshots, failure placeholders, resume by daily completion markers, Cookie login warnings, daily SQLite backups, Shanghai daily scheduling, and CLI snapshot/auth-check commands. Real read-only run captured 795/795 videos successfully over 1124s with a verified daily backup.
2026-10-31 19:19:24 +08:00

168 lines
4.8 KiB
Python

# -*- coding: utf-8 -*-
"""B 站收藏夹接口的返回模型。"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping
@dataclass(frozen=True)
class FavId:
"""``fav/resource/ids`` 的一条资源。"""
aid: int
type: int
bvid: str | None = None
@dataclass(frozen=True)
class FavResource:
"""``fav/resource/infos`` 的一条视频元数据。"""
aid: int
bvid: str | None
title: str
up_mid: int | None
up_name: str | None
pubdate: int
duration: int | None
cover: str | None
attr: int
fav_time: int | None
@property
def invalid(self) -> bool:
# 实测 attr=9 表示"已失效视频"。
return self.attr == 9
@classmethod
def from_api(cls, item: Mapping[str, Any]) -> "FavResource":
aid = item.get("id")
if isinstance(aid, bool) or not isinstance(aid, int):
raise ValueError("infos item.id 不是整数")
upper = item.get("upper") or {}
if not isinstance(upper, Mapping):
upper = {}
up_mid = upper.get("mid")
if isinstance(up_mid, bool) or not isinstance(up_mid, int):
up_mid = None
raw_duration = item.get("duration")
duration = raw_duration if isinstance(raw_duration, int) and not isinstance(
raw_duration, bool
) else None
raw_attr = item.get("attr", 0)
attr = raw_attr if isinstance(raw_attr, int) and not isinstance(raw_attr, bool) else 0
fav_time = item.get("fav_time")
if isinstance(fav_time, bool) or not isinstance(fav_time, int) or fav_time <= 0:
fav_time = None
bvid = item.get("bvid") or item.get("bv_id")
if not isinstance(bvid, str) or not bvid:
bvid = None
return cls(
aid=aid,
bvid=bvid,
title=str(item.get("title") or ""),
up_mid=up_mid,
up_name=str(upper.get("name") or "") or None,
pubdate=int(item.get("pubtime") or item.get("ctime") or 0),
duration=duration,
cover=str(item.get("cover") or "") or None,
attr=attr,
fav_time=fav_time,
)
@dataclass(frozen=True)
class FolderInfo:
media_id: int
title: str
media_count: int
@dataclass(frozen=True)
class InfosResult:
requested: tuple[int, ...]
items: dict[int, FavResource]
missing: tuple[int, ...]
@dataclass(frozen=True)
class ViewStat:
"""``x/web-interface/view`` 的七项统计。"""
aid: int
bvid: str | None
title: str
view: int
like: int
favorite: int
coin: int
share: int
reply: int
danmaku: int
def metrics(self) -> dict[str, int]:
return {
"view": self.view,
"like": self.like,
"favorite": self.favorite,
"coin": self.coin,
"share": self.share,
"reply": self.reply,
"danmaku": self.danmaku,
}
@classmethod
def from_api(cls, item: Mapping[str, Any]) -> "ViewStat":
aid = item.get("aid")
if isinstance(aid, bool) or not isinstance(aid, int):
raise ValueError("view data.aid 不是整数")
stat = item.get("stat")
if not isinstance(stat, Mapping):
raise ValueError("view data.stat 不是对象")
metrics: dict[str, int] = {}
for key in ("view", "like", "favorite", "coin", "share", "reply", "danmaku"):
value = stat.get(key)
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise ValueError(f"view stat.{key} 不是非负整数")
metrics[key] = value
bvid = item.get("bvid")
if not isinstance(bvid, str) or not bvid:
bvid = None
return cls(
aid=aid,
bvid=bvid,
title=str(item.get("title") or ""),
**metrics,
)
@dataclass(frozen=True)
class FanStat:
mid: int
follower: int
following: int
@classmethod
def from_api(cls, item: Mapping[str, Any]) -> "FanStat":
mid = item.get("mid")
follower = item.get("follower")
following = item.get("following", 0)
if isinstance(mid, bool) or not isinstance(mid, int):
raise ValueError("relation/stat data.mid 不是整数")
if isinstance(follower, bool) or not isinstance(follower, int) or follower < 0:
raise ValueError("relation/stat data.follower 不是非负整数")
if isinstance(following, bool) or not isinstance(following, int) or following < 0:
raise ValueError("relation/stat data.following 不是非负整数")
return cls(mid=mid, follower=follower, following=following)
@dataclass(frozen=True)
class LoginState:
is_login: bool
mid: int | None
uname: str | None
code: int
message: str