fix: close data integrity and hot-reload review findings
Backfill same-day snapshots for newly added or restored videos, preserve successful fan snapshots on resume, persist incomplete state before forced re-runs, and split collection completion from backup completion so failed backups can retry without API calls. Reject missing data payloads instead of treating them as empty folders, guard against mass-unfav on suspicious empty remote folders, rebuild Bilibili clients when credentials change, recompute primary folder ownership from active relations with per-folder fav_time, preserve original titles for dropped invalid videos, poll config every 60 seconds, and fix favsort verify interval and mixed resource types.
This commit is contained in:
1 parent
178ab64b18
commit
6a21b5d905
21 files changed
+895
-82
No files matched your search
@@ -262,6 +262,20 @@ class BilibiliClient:
|
||||
raise self._api_error(payload)
|
||||
return payload
|
||||
|
||||
@staticmethod
|
||||
def _data_list(payload: Mapping[str, Any], name: str) -> list[Any]:
|
||||
"""区分“真实空数组”与“协议缺字段”。
|
||||
|
||||
实测空收藏夹返回 ``{"code":0,"data":[]}``;缺少 ``data`` 或为 ``null``
|
||||
都不是已知契约,必须中止,避免被误判成空收藏夹后批量改状态。
|
||||
"""
|
||||
if "data" not in payload:
|
||||
raise BilibiliProtocolError(f"{name} 响应缺少 data 字段")
|
||||
data = payload["data"]
|
||||
if not isinstance(data, list):
|
||||
raise BilibiliProtocolError(f"{name} data 不是数组")
|
||||
return data
|
||||
|
||||
# ------------------------------------------------------------ fav
|
||||
|
||||
def fav_ids(self, media_id: int) -> list[FavId]:
|
||||
@@ -269,11 +283,7 @@ class BilibiliClient:
|
||||
f"{BASE}/x/v3/fav/resource/ids?media_id={int(media_id)}&platform=web"
|
||||
)
|
||||
payload = self.get_json(url)
|
||||
data = payload.get("data")
|
||||
if data is None:
|
||||
data = []
|
||||
if not isinstance(data, list):
|
||||
raise BilibiliProtocolError("fav/resource/ids data 不是数组")
|
||||
data = self._data_list(payload, "fav/resource/ids")
|
||||
out: list[FavId] = []
|
||||
for index, item in enumerate(data):
|
||||
if not isinstance(item, Mapping):
|
||||
@@ -320,11 +330,7 @@ class BilibiliClient:
|
||||
f"&platform=web&folder_mid={fmid}&folder_id={int(media_id)}"
|
||||
)
|
||||
payload = self.get_json(url)
|
||||
data = payload.get("data")
|
||||
if data is None:
|
||||
data = []
|
||||
if not isinstance(data, list):
|
||||
raise BilibiliProtocolError("fav/resource/infos data 不是数组")
|
||||
data = self._data_list(payload, "fav/resource/infos")
|
||||
chunk_set = set(chunk)
|
||||
for item in data:
|
||||
if not isinstance(item, Mapping):
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""按账号角色提供 B 站客户端,并在凭据变化时自动重建。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
from ..config import ConfigStore
|
||||
from .client import BilibiliClient
|
||||
|
||||
|
||||
class ClientProvider:
|
||||
"""缓存客户端;仅当 ``mid`` / ``cookie`` / ``csrf`` 变化时重建。
|
||||
|
||||
``factory`` 负责固化该角色的限速与重试策略,因此重建不会丢失这些设置。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
store: ConfigStore,
|
||||
role: str,
|
||||
factory: Callable[[Any], BilibiliClient],
|
||||
) -> None:
|
||||
if role not in {"read", "write"}:
|
||||
raise ValueError(f"未知账号角色: {role}")
|
||||
self.store = store
|
||||
self.role = role
|
||||
self.factory = factory
|
||||
self._key: tuple[int, str, str] | None = None
|
||||
self._client: BilibiliClient | None = None
|
||||
|
||||
def get(self) -> BilibiliClient:
|
||||
credentials = getattr(self.store.get().bilibili, self.role)
|
||||
key = (credentials.mid, credentials.cookie, credentials.csrf)
|
||||
if self._client is None or key != self._key:
|
||||
self._client = self.factory(credentials)
|
||||
self._key = key
|
||||
return self._client
|
||||
Reference in new issue
Block a user