feat(p0): add favorite folder sync and sqlite data foundation
Implement P0 of the reaction dashboard: config loading with hot reload, SQLite migrations with backups, single-writer queue, read-only Bilibili fav client, folder import/diff sync, invalid-resource handling, blacklist seed, scheduler, and JSON redacted logging. Includes offline unit and transport-level contract tests; all existing and new tests pass.
This commit is contained in:
commit
e2cdf997af
39 files changed
+8575
No files matched your search
@@ -0,0 +1,87 @@
|
||||
# -*- 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, ...]
|
||||
Reference in new issue
Block a user