259 lines
7.4 KiB
Python
259 lines
7.4 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
|
|
pages: int | None = 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_pages = item.get("page")
|
|
pages = raw_pages if (
|
|
isinstance(raw_pages, int)
|
|
and not isinstance(raw_pages, bool)
|
|
and raw_pages > 0
|
|
) 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,
|
|
pages=pages,
|
|
)
|
|
|
|
|
|
@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 VideoDetail:
|
|
"""``x/web-interface/view`` 的完整元数据 + 七项统计。
|
|
|
|
比 :class:`ViewStat` 多出 UP、时长、分 P 数、封面等字段,供手动入库建库使用。
|
|
"""
|
|
|
|
aid: int
|
|
bvid: str | None
|
|
title: str
|
|
up_mid: int | None
|
|
up_name: str | None
|
|
pubdate: int
|
|
duration: int
|
|
pages: int | None
|
|
cover: str | None
|
|
stat: ViewStat
|
|
|
|
def metrics(self) -> dict[str, int]:
|
|
return self.stat.metrics()
|
|
|
|
@classmethod
|
|
def from_api(cls, item: Mapping[str, Any]) -> "VideoDetail":
|
|
stat = ViewStat.from_api(item)
|
|
owner = item.get("owner") or {}
|
|
if not isinstance(owner, Mapping):
|
|
owner = {}
|
|
raw_mid = owner.get("mid")
|
|
up_mid = (
|
|
raw_mid
|
|
if isinstance(raw_mid, int) and not isinstance(raw_mid, bool)
|
|
else None
|
|
)
|
|
up_name = owner.get("name")
|
|
if not isinstance(up_name, str) or not up_name:
|
|
up_name = None
|
|
|
|
raw_duration = item.get("duration")
|
|
duration = (
|
|
raw_duration
|
|
if isinstance(raw_duration, int)
|
|
and not isinstance(raw_duration, bool)
|
|
and raw_duration >= 0
|
|
else 0
|
|
)
|
|
|
|
pages: int | None = None
|
|
raw_pages = item.get("pages")
|
|
if isinstance(raw_pages, list) and raw_pages:
|
|
pages = len(raw_pages)
|
|
else:
|
|
raw_videos = item.get("videos")
|
|
if (
|
|
isinstance(raw_videos, int)
|
|
and not isinstance(raw_videos, bool)
|
|
and raw_videos > 0
|
|
):
|
|
pages = raw_videos
|
|
|
|
raw_pubdate = item.get("pubdate") or item.get("ctime")
|
|
pubdate = (
|
|
raw_pubdate
|
|
if isinstance(raw_pubdate, int) and not isinstance(raw_pubdate, bool)
|
|
else 0
|
|
)
|
|
cover = item.get("pic")
|
|
if not isinstance(cover, str) or not cover:
|
|
cover = None
|
|
|
|
return cls(
|
|
aid=stat.aid,
|
|
bvid=stat.bvid,
|
|
title=stat.title,
|
|
up_mid=up_mid,
|
|
up_name=up_name,
|
|
pubdate=pubdate,
|
|
duration=duration,
|
|
pages=pages,
|
|
cover=cover,
|
|
stat=stat,
|
|
)
|
|
|
|
|
|
@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
|