feat(snapshot): refresh API video metadata; keep removed visible #9
No files matched your search
@@ -410,6 +410,75 @@ class Repository:
|
|||||||
return [dict(row) for row in rows]
|
return [dict(row) for row in rows]
|
||||||
return self.db.read(run)
|
return self.db.read(run)
|
||||||
|
|
||||||
|
def refresh_video_metadata(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
aid: int,
|
||||||
|
bvid: str | None,
|
||||||
|
title: str,
|
||||||
|
up_mid: int | None,
|
||||||
|
up_name: str | None,
|
||||||
|
pubdate: int,
|
||||||
|
duration: int | None = None,
|
||||||
|
pages: int | None = None,
|
||||||
|
cover: str | None = None,
|
||||||
|
up_face: str | None = None,
|
||||||
|
now: int | None = None,
|
||||||
|
) -> bool:
|
||||||
|
"""用接口返回字段覆盖更新已有视频元数据;不改 status,空值不冲掉旧值。
|
||||||
|
|
||||||
|
入库与快照成功路径共用。视频不存在时返回 False。
|
||||||
|
"""
|
||||||
|
now = self._now() if now is None else int(now)
|
||||||
|
aid = int(aid)
|
||||||
|
|
||||||
|
def run(conn: sqlite3.Connection) -> bool:
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT aid FROM video WHERE aid = ?", (aid,)
|
||||||
|
).fetchone()
|
||||||
|
if existing is None:
|
||||||
|
return False
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE video SET
|
||||||
|
bvid = COALESCE(?, bvid),
|
||||||
|
title = CASE
|
||||||
|
WHEN ? != '' THEN ?
|
||||||
|
ELSE title
|
||||||
|
END,
|
||||||
|
up_mid = COALESCE(?, up_mid),
|
||||||
|
up_name = COALESCE(?, up_name),
|
||||||
|
pubdate = CASE
|
||||||
|
WHEN ? > 0 THEN ?
|
||||||
|
ELSE pubdate
|
||||||
|
END,
|
||||||
|
duration = COALESCE(?, duration),
|
||||||
|
pages = COALESCE(?, pages),
|
||||||
|
cover = COALESCE(?, cover),
|
||||||
|
up_face = COALESCE(?, up_face),
|
||||||
|
last_sync = ?
|
||||||
|
WHERE aid = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
bvid,
|
||||||
|
title or "",
|
||||||
|
title or "(无标题)",
|
||||||
|
up_mid,
|
||||||
|
up_name,
|
||||||
|
int(pubdate or 0),
|
||||||
|
int(pubdate or 0),
|
||||||
|
None if duration is None else int(duration),
|
||||||
|
None if pages is None else int(pages),
|
||||||
|
cover,
|
||||||
|
up_face,
|
||||||
|
now,
|
||||||
|
aid,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return self.db.write(run)
|
||||||
|
|
||||||
def ingest_video(
|
def ingest_video(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -430,6 +499,7 @@ class Repository:
|
|||||||
|
|
||||||
不写 B 站收藏夹、不设置 ``folder_id``;同时删除同 aid 的候选行。
|
不写 B 站收藏夹、不设置 ``folder_id``;同时删除同 aid 的候选行。
|
||||||
既有的 ``removed`` / ``dropped`` / ``unfaved`` 状态都会被重新激活。
|
既有的 ``removed`` / ``dropped`` / ``unfaved`` 状态都会被重新激活。
|
||||||
|
已存在时仍以本次接口字段为准刷新元数据。
|
||||||
"""
|
"""
|
||||||
now = self._now() if now is None else int(now)
|
now = self._now() if now is None else int(now)
|
||||||
aid = int(aid)
|
aid = int(aid)
|
||||||
|
|||||||
@@ -116,11 +116,37 @@ class LibraryService:
|
|||||||
if candidate is None:
|
if candidate is None:
|
||||||
video = self.repo.get_video(aid)
|
video = self.repo.get_video(aid)
|
||||||
if video is not None:
|
if video is not None:
|
||||||
|
# 已在库:仍以接口为准刷新元数据(不重复建档)
|
||||||
|
try:
|
||||||
|
detail = self.read_provider.get().view_detail(aid=aid)
|
||||||
|
except BilibiliError as exc:
|
||||||
|
self._log("warning", "ingest.detail_failed", aid=aid, error=str(exc))
|
||||||
|
return IngestResult(
|
||||||
|
ok=True, aid=aid, state="exists",
|
||||||
|
title=str(video.get("title") or ""),
|
||||||
|
up_name=str(video.get("up_name") or ""),
|
||||||
|
message="已在库,无需重复入库",
|
||||||
|
)
|
||||||
|
self.repo.refresh_video_metadata(
|
||||||
|
aid=detail.aid,
|
||||||
|
bvid=detail.bvid,
|
||||||
|
title=detail.title,
|
||||||
|
up_mid=detail.up_mid,
|
||||||
|
up_name=detail.up_name,
|
||||||
|
pubdate=detail.pubdate,
|
||||||
|
duration=detail.duration,
|
||||||
|
pages=detail.pages,
|
||||||
|
cover=detail.cover,
|
||||||
|
up_face=detail.up_face,
|
||||||
|
now=self._now(),
|
||||||
|
)
|
||||||
|
self._write_snapshot(detail)
|
||||||
|
self.repo.bump_cache_version("videos")
|
||||||
return IngestResult(
|
return IngestResult(
|
||||||
ok=True, aid=aid, state="exists",
|
ok=True, aid=aid, state="exists",
|
||||||
title=str(video.get("title") or ""),
|
title=detail.title,
|
||||||
up_name=str(video.get("up_name") or ""),
|
up_name=detail.up_name or "",
|
||||||
message="已在库,无需重复入库",
|
message="已在库,已刷新元数据",
|
||||||
)
|
)
|
||||||
return IngestResult(
|
return IngestResult(
|
||||||
ok=False, aid=aid, state="missing", message="候选不存在或已处理"
|
ok=False, aid=aid, state="missing", message="候选不存在或已处理"
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
|
|
||||||
规则(DEVELOPMENT.md §7.4):
|
规则(DEVELOPMENT.md §7.4):
|
||||||
|
|
||||||
* 对 ``active`` / ``unfaved`` 视频逐条请求 ``view``;
|
* 对 ``active`` / ``unfaved`` 视频逐条请求 ``view_detail``(完整元数据 + 七项);
|
||||||
* 成功写入七项指标;``ok=1`` 的同日快照直接跳过(断点续跑;含小时任务 1 小时内刚刷新的);
|
* 成功时以接口字段覆盖更新 ``video`` 元数据(title/duration/pages/cover/UP 等),并写入七项快照;
|
||||||
|
* ``ok=1`` 的同日快照直接跳过(断点续跑;含小时任务 1 小时内刚刷新的);
|
||||||
* 午夜 ``seed_at`` 用上一完成日 ok=1 行克隆出当日壳(``snapshot:seeded`` / ``fans:seeded``),不推进完成日;
|
* 午夜 ``seed_at`` 用上一完成日 ok=1 行克隆出当日壳(``snapshot:seeded`` / ``fans:seeded``),不推进完成日;
|
||||||
* 晚间 ``snapshot_at`` 全量必须 force 打穿 seed,成功后才写完成日并清除 seeded;
|
* 晚间 ``snapshot_at`` 全量必须 force 打穿 seed,成功后才写完成日并清除 seeded;
|
||||||
* ``run_recent_videos`` 只刷新近 N 天新片的当日行,不改完成日 / 备份;
|
* ``run_recent_videos`` 只刷新近 N 天新片的当日行,不改完成日 / 备份;
|
||||||
* 视频不可访问时标记 ``removed``;
|
* 视频不可访问时标记 ``removed``(保留元数据与历史快照,站内继续展示,后续采集跳过);
|
||||||
* 其他错误最多重试 3 次,仍失败写 ``ok=0`` 占位,不写假零值;
|
* 其他错误最多重试 3 次,仍失败写 ``ok=0`` 占位,不写假零值;
|
||||||
* 对 ``track_fans`` 逐个请求 ``relation/stat``(独立任务,不影响视频完成日);
|
* 对 ``track_fans`` 逐个请求 ``relation/stat``(独立任务,不影响视频完成日);
|
||||||
* 视频采集全部成功后写入当日完成标记并生成日备份。
|
* 视频采集全部成功后写入当日完成标记并生成日备份。
|
||||||
@@ -224,7 +225,7 @@ class SnapshotService:
|
|||||||
force: bool = False,
|
force: bool = False,
|
||||||
keep_scraped: bool = False,
|
keep_scraped: bool = False,
|
||||||
) -> SnapshotSummary:
|
) -> SnapshotSummary:
|
||||||
"""采集视频七项指标并在全部成功后标记完成日 / 备份。
|
"""采集视频元数据 + 七项指标,并在全部成功后标记完成日 / 备份。
|
||||||
|
|
||||||
``force``:忽略完成标记,重跑。
|
``force``:忽略完成标记,重跑。
|
||||||
``keep_scraped``:force 时仍跳过非 seed 的 ok=1(seal 断点续跑);
|
``keep_scraped``:force 时仍跳过非 seed 的 ok=1(seal 断点续跑);
|
||||||
@@ -264,7 +265,7 @@ class SnapshotService:
|
|||||||
continue
|
continue
|
||||||
# force 且非 keep_scraped:CLI 全量重采,不 skip
|
# force 且非 keep_scraped:CLI 全量重采,不 skip
|
||||||
|
|
||||||
stat, error, unavailable = self._fetch_view(client, aid)
|
detail, error, unavailable = self._fetch_view_detail(client, aid)
|
||||||
if unavailable:
|
if unavailable:
|
||||||
summary.removed += 1
|
summary.removed += 1
|
||||||
self.repo.set_video_status(aid, "removed", now=now)
|
self.repo.set_video_status(aid, "removed", now=now)
|
||||||
@@ -272,17 +273,16 @@ class SnapshotService:
|
|||||||
"warning", "snapshot.removed",
|
"warning", "snapshot.removed",
|
||||||
stat_date=stat_date, aid=aid, error=error,
|
stat_date=stat_date, aid=aid, error=error,
|
||||||
)
|
)
|
||||||
elif stat is not None:
|
elif detail is not None:
|
||||||
|
self._apply_detail(detail, now=self._now())
|
||||||
self.repo.upsert_snapshot(
|
self.repo.upsert_snapshot(
|
||||||
stat_date,
|
stat_date,
|
||||||
aid,
|
aid,
|
||||||
metrics=stat.metrics(),
|
metrics=detail.metrics(),
|
||||||
ok=True,
|
ok=True,
|
||||||
err=None,
|
err=None,
|
||||||
captured_at=self._now(),
|
captured_at=self._now(),
|
||||||
)
|
)
|
||||||
if getattr(stat, "up_face", None):
|
|
||||||
self.repo.apply_video_up_face(aid, stat.up_face)
|
|
||||||
summary.success += 1
|
summary.success += 1
|
||||||
else:
|
else:
|
||||||
summary.failed += 1
|
summary.failed += 1
|
||||||
@@ -369,7 +369,7 @@ class SnapshotService:
|
|||||||
videos = self.repo.video_aids_recent_for_snapshot(since_pubdate=since_pubdate)
|
videos = self.repo.video_aids_recent_for_snapshot(since_pubdate=since_pubdate)
|
||||||
summary.total = len(videos)
|
summary.total = len(videos)
|
||||||
for index, aid in enumerate(videos, 1):
|
for index, aid in enumerate(videos, 1):
|
||||||
stat, error, unavailable = self._fetch_view(client, aid)
|
detail, error, unavailable = self._fetch_view_detail(client, aid)
|
||||||
if unavailable:
|
if unavailable:
|
||||||
summary.removed += 1
|
summary.removed += 1
|
||||||
self.repo.set_video_status(aid, "removed", now=now)
|
self.repo.set_video_status(aid, "removed", now=now)
|
||||||
@@ -377,17 +377,16 @@ class SnapshotService:
|
|||||||
"warning", "snapshot.recent_removed",
|
"warning", "snapshot.recent_removed",
|
||||||
stat_date=stat_date, aid=aid, error=error,
|
stat_date=stat_date, aid=aid, error=error,
|
||||||
)
|
)
|
||||||
elif stat is not None:
|
elif detail is not None:
|
||||||
|
self._apply_detail(detail, now=self._now())
|
||||||
self.repo.upsert_snapshot(
|
self.repo.upsert_snapshot(
|
||||||
stat_date,
|
stat_date,
|
||||||
aid,
|
aid,
|
||||||
metrics=stat.metrics(),
|
metrics=detail.metrics(),
|
||||||
ok=True,
|
ok=True,
|
||||||
err=None,
|
err=None,
|
||||||
captured_at=self._now(),
|
captured_at=self._now(),
|
||||||
)
|
)
|
||||||
if getattr(stat, "up_face", None):
|
|
||||||
self.repo.apply_video_up_face(aid, stat.up_face)
|
|
||||||
summary.success += 1
|
summary.success += 1
|
||||||
else:
|
else:
|
||||||
summary.failed += 1
|
summary.failed += 1
|
||||||
@@ -693,11 +692,27 @@ class SnapshotService:
|
|||||||
)
|
)
|
||||||
return state.is_login, state.message
|
return state.is_login, state.message
|
||||||
|
|
||||||
def _fetch_view(self, client, aid: int):
|
def _apply_detail(self, detail, *, now: int) -> None:
|
||||||
|
"""快照成功:以 view_detail 字段覆盖 video 元数据(不改 status)。"""
|
||||||
|
self.repo.refresh_video_metadata(
|
||||||
|
aid=detail.aid,
|
||||||
|
bvid=detail.bvid,
|
||||||
|
title=detail.title,
|
||||||
|
up_mid=detail.up_mid,
|
||||||
|
up_name=detail.up_name,
|
||||||
|
pubdate=detail.pubdate,
|
||||||
|
duration=detail.duration,
|
||||||
|
pages=detail.pages,
|
||||||
|
cover=detail.cover,
|
||||||
|
up_face=detail.up_face,
|
||||||
|
now=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fetch_view_detail(self, client, aid: int):
|
||||||
last_error = ""
|
last_error = ""
|
||||||
for attempt in range(1, self.retries + 1):
|
for attempt in range(1, self.retries + 1):
|
||||||
try:
|
try:
|
||||||
return client.view(aid), None, False
|
return client.view_detail(aid=aid), None, False
|
||||||
except BilibiliAPIError as exc:
|
except BilibiliAPIError as exc:
|
||||||
last_error = str(exc)
|
last_error = str(exc)
|
||||||
if exc.code in VIDEO_UNAVAILABLE_CODES:
|
if exc.code in VIDEO_UNAVAILABLE_CODES:
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ from ..timeutil import (
|
|||||||
from .cache import DASHBOARD_CACHE, TTL_FANS, TTL_VIDEOS, build_cache_key
|
from .cache import DASHBOARD_CACHE, TTL_FANS, TTL_VIDEOS, build_cache_key
|
||||||
|
|
||||||
TRACKED_STATUSES = ("active", "unfaved")
|
TRACKED_STATUSES = ("active", "unfaved")
|
||||||
|
# 站内列表可见:含原站失效 removed(继续展示、badge 可标 status-removed);不含人工 dropped
|
||||||
|
VISIBLE_STATUSES = ("active", "unfaved", "removed")
|
||||||
VIDEO_SORTS = {
|
VIDEO_SORTS = {
|
||||||
"heat", "view", "like", "coin", "favorite", "share", "reply", "danmaku",
|
"heat", "view", "like", "coin", "favorite", "share", "reply", "danmaku",
|
||||||
"interaction", "growth", "growth_rate", "added", "published",
|
"interaction", "growth", "growth_rate", "added", "published",
|
||||||
@@ -955,6 +957,7 @@ class DashboardQueries:
|
|||||||
within_start = _date_minus(latest, int(within_days) - 1)
|
within_start = _date_minus(latest, int(within_days) - 1)
|
||||||
videos = self._videos(
|
videos = self._videos(
|
||||||
conn,
|
conn,
|
||||||
|
statuses=VISIBLE_STATUSES,
|
||||||
folder_id=folder_id,
|
folder_id=folder_id,
|
||||||
q=q,
|
q=q,
|
||||||
within_field=within_field,
|
within_field=within_field,
|
||||||
@@ -1157,7 +1160,7 @@ class DashboardQueries:
|
|||||||
aid = int(aid)
|
aid = int(aid)
|
||||||
|
|
||||||
def run(conn: sqlite3.Connection) -> dict[str, Any] | None:
|
def run(conn: sqlite3.Connection) -> dict[str, Any] | None:
|
||||||
video_rows = self._videos(conn)
|
video_rows = self._videos(conn, statuses=VISIBLE_STATUSES)
|
||||||
video = next((item for item in video_rows if int(item["aid"]) == aid), None)
|
video = next((item for item in video_rows if int(item["aid"]) == aid), None)
|
||||||
if video is None:
|
if video is None:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
|
|||||||
@@ -256,11 +256,41 @@ def view_for(
|
|||||||
reply: int = 16,
|
reply: int = 16,
|
||||||
danmaku: int = 7,
|
danmaku: int = 7,
|
||||||
bvid: str | None = None,
|
bvid: str | None = None,
|
||||||
|
title: str | None = None,
|
||||||
|
duration: int | None = None,
|
||||||
|
pages: int | None = None,
|
||||||
|
cover: str | None = None,
|
||||||
|
up_mid: int | None = None,
|
||||||
|
up_name: str | None = None,
|
||||||
|
up_face: str | None = None,
|
||||||
|
pubdate: int | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
item = copy.deepcopy(VIEW_PAYLOAD)
|
item = copy.deepcopy(VIEW_PAYLOAD)
|
||||||
data = item["data"]
|
data = item["data"]
|
||||||
data["aid"] = aid
|
data["aid"] = aid
|
||||||
data["bvid"] = bvid or f"BV{aid}"
|
data["bvid"] = bvid or f"BV{aid}"
|
||||||
|
if title is not None:
|
||||||
|
data["title"] = title
|
||||||
|
if duration is not None:
|
||||||
|
data["duration"] = duration
|
||||||
|
if cover is not None:
|
||||||
|
data["pic"] = cover
|
||||||
|
if pubdate is not None:
|
||||||
|
data["pubdate"] = pubdate
|
||||||
|
data["ctime"] = pubdate
|
||||||
|
if pages is not None:
|
||||||
|
data["videos"] = pages
|
||||||
|
data["pages"] = [
|
||||||
|
{"cid": 1000 + i, "page": i + 1, "from": "vupload", "part": f"P{i + 1}"}
|
||||||
|
for i in range(max(1, int(pages)))
|
||||||
|
]
|
||||||
|
owner = data.setdefault("owner", {})
|
||||||
|
if up_mid is not None:
|
||||||
|
owner["mid"] = up_mid
|
||||||
|
if up_name is not None:
|
||||||
|
owner["name"] = up_name
|
||||||
|
if up_face is not None:
|
||||||
|
owner["face"] = up_face
|
||||||
data["stat"].update({
|
data["stat"].update({
|
||||||
"aid": aid,
|
"aid": aid,
|
||||||
"view": view,
|
"view": view,
|
||||||
|
|||||||
@@ -408,6 +408,72 @@ def test_up_follower_snapshot_and_backfill():
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_video_metadata_overwrites_api_fields_keeps_nulls():
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
db, repo = setup(tmp)
|
||||||
|
try:
|
||||||
|
repo.upsert_from_fav(resource(9, pages=1), F1.media_id, was_added=True)
|
||||||
|
ok = repo.refresh_video_metadata(
|
||||||
|
aid=9,
|
||||||
|
bvid="BVnew9",
|
||||||
|
title="刷新后",
|
||||||
|
up_mid=200,
|
||||||
|
up_name="新名",
|
||||||
|
pubdate=1_710_000_000,
|
||||||
|
duration=999,
|
||||||
|
pages=5,
|
||||||
|
cover="http://example/new.png",
|
||||||
|
up_face="http://example/face.png",
|
||||||
|
now=1_800_000_100,
|
||||||
|
)
|
||||||
|
assert ok is True
|
||||||
|
video = repo.get_video(9)
|
||||||
|
assert video["title"] == "刷新后"
|
||||||
|
assert video["duration"] == 999
|
||||||
|
assert video["pages"] == 5
|
||||||
|
assert video["bvid"] == "BVnew9"
|
||||||
|
assert video["up_mid"] == 200
|
||||||
|
assert video["cover"] == "http://example/new.png"
|
||||||
|
assert video["status"] == "active"
|
||||||
|
# 空值不冲掉
|
||||||
|
ok = repo.refresh_video_metadata(
|
||||||
|
aid=9,
|
||||||
|
bvid=None,
|
||||||
|
title="",
|
||||||
|
up_mid=None,
|
||||||
|
up_name=None,
|
||||||
|
pubdate=0,
|
||||||
|
duration=None,
|
||||||
|
pages=None,
|
||||||
|
cover=None,
|
||||||
|
up_face=None,
|
||||||
|
now=1_800_000_200,
|
||||||
|
)
|
||||||
|
assert ok is True
|
||||||
|
video = repo.get_video(9)
|
||||||
|
assert video["title"] == "刷新后"
|
||||||
|
assert video["duration"] == 999
|
||||||
|
assert video["pages"] == 5
|
||||||
|
assert video["bvid"] == "BVnew9"
|
||||||
|
# 不存在的 aid
|
||||||
|
assert repo.refresh_video_metadata(
|
||||||
|
aid=404,
|
||||||
|
bvid=None, title="x", up_mid=None, up_name=None, pubdate=1,
|
||||||
|
) is False
|
||||||
|
# removed 后 refresh 也不改 status
|
||||||
|
repo.set_video_status(9, "removed")
|
||||||
|
repo.refresh_video_metadata(
|
||||||
|
aid=9, bvid="BVkeep", title="仍在", up_mid=200, up_name="新名",
|
||||||
|
pubdate=1_710_000_000, duration=1000, pages=6,
|
||||||
|
)
|
||||||
|
video = repo.get_video(9)
|
||||||
|
assert video["status"] == "removed"
|
||||||
|
assert video["title"] == "仍在"
|
||||||
|
assert video["pages"] == 6
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
test_upsert_and_folder_resource()
|
test_upsert_and_folder_resource()
|
||||||
test_invalid_attr_and_missing()
|
test_invalid_attr_and_missing()
|
||||||
@@ -422,8 +488,10 @@ def main():
|
|||||||
test_pending_snapshot_aids()
|
test_pending_snapshot_aids()
|
||||||
test_upsert_snapshot_preserves_view_open()
|
test_upsert_snapshot_preserves_view_open()
|
||||||
test_up_follower_snapshot_and_backfill()
|
test_up_follower_snapshot_and_backfill()
|
||||||
|
test_refresh_video_metadata_overwrites_api_fields_keeps_nulls()
|
||||||
print("OK: 收藏夹/视频状态机与黑名单测试全部通过")
|
print("OK: 收藏夹/视频状态机与黑名单测试全部通过")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
+81
-6
@@ -216,14 +216,86 @@ def test_unavailable_marks_removed():
|
|||||||
try:
|
try:
|
||||||
add_video(repo, 1)
|
add_video(repo, 1)
|
||||||
add_video(repo, 2)
|
add_video(repo, 2)
|
||||||
|
before = repo.get_video(1)
|
||||||
result = service.run(date=DATE)
|
result = service.run(date=DATE)
|
||||||
assert result.removed == 2
|
assert result.removed == 2
|
||||||
assert result.failed == 0
|
assert result.failed == 0
|
||||||
assert result.completed is True
|
assert result.completed is True
|
||||||
assert repo.get_video(1)["status"] == "removed"
|
video1 = repo.get_video(1)
|
||||||
assert repo.get_video(2)["status"] == "removed"
|
video2 = repo.get_video(2)
|
||||||
|
assert video1["status"] == "removed"
|
||||||
|
assert video2["status"] == "removed"
|
||||||
|
# 失效只改 status,不得抹元数据 / 不得写空快照
|
||||||
|
assert video1["title"] == before["title"]
|
||||||
|
assert video1["duration"] == before["duration"]
|
||||||
|
assert video1["bvid"] == before["bvid"]
|
||||||
assert repo.get_snapshot(DATE, 1) is None
|
assert repo.get_snapshot(DATE, 1) is None
|
||||||
assert repo.get_snapshot(DATE, 2) is None
|
assert repo.get_snapshot(DATE, 2) is None
|
||||||
|
# removed 不再进入后续快照队列
|
||||||
|
assert 1 not in repo.video_aids_for_snapshot()
|
||||||
|
assert 2 not in repo.video_aids_for_snapshot()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_snapshot_refreshes_video_metadata():
|
||||||
|
"""快照成功路径以 view_detail 覆盖 pages/duration/title 等元数据。"""
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
transport = SnapshotTransport({
|
||||||
|
1: view_for(
|
||||||
|
1, view=111, title="新标题P3", duration=3600, pages=3,
|
||||||
|
cover="http://example/new.jpg", up_name="新UP",
|
||||||
|
),
|
||||||
|
})
|
||||||
|
db, repo, service = make_service(tmp, transport, track_fans=())
|
||||||
|
try:
|
||||||
|
add_video(repo, 1)
|
||||||
|
old = repo.get_video(1)
|
||||||
|
assert old["title"] == "视频1"
|
||||||
|
assert old["duration"] == 600
|
||||||
|
assert old.get("pages") in (None, 0) or old["pages"] != 3
|
||||||
|
result = service.run_videos(date=DATE)
|
||||||
|
assert result.success == 1
|
||||||
|
video = repo.get_video(1)
|
||||||
|
assert video["title"] == "新标题P3"
|
||||||
|
assert video["duration"] == 3600
|
||||||
|
assert video["pages"] == 3
|
||||||
|
assert video["cover"] == "http://example/new.jpg"
|
||||||
|
assert video["up_name"] == "新UP"
|
||||||
|
assert video["status"] == "active"
|
||||||
|
snap = repo.get_snapshot(DATE, 1)
|
||||||
|
assert snap["view"] == 111 and snap["ok"] == 1
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_snapshot_refreshes_metadata():
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
now = 1_800_000_000
|
||||||
|
transport = SnapshotTransport({
|
||||||
|
7: view_for(7, view=77, title="近新片加P", duration=1200, pages=2,
|
||||||
|
pubdate=now - 3600),
|
||||||
|
})
|
||||||
|
db, repo, service = make_service(tmp, transport, track_fans=())
|
||||||
|
try:
|
||||||
|
repo.upsert_from_fav(
|
||||||
|
FavResource(
|
||||||
|
aid=7, bvid="BV7", title="旧标题", up_mid=100,
|
||||||
|
up_name="UP", pubdate=now - 3600, duration=300,
|
||||||
|
cover=None, attr=0, fav_time=None, pages=1,
|
||||||
|
),
|
||||||
|
FOLDER.media_id,
|
||||||
|
was_added=True,
|
||||||
|
)
|
||||||
|
result = service.run_recent_videos(date=DATE, recent_days=2)
|
||||||
|
assert result.success == 1
|
||||||
|
video = repo.get_video(7)
|
||||||
|
assert video["title"] == "近新片加P"
|
||||||
|
assert video["duration"] == 1200
|
||||||
|
assert video["pages"] == 2
|
||||||
|
assert repo.get_snapshot(DATE, 7)["view"] == 77
|
||||||
|
# recent 不推进完成日
|
||||||
|
assert repo.kv_get("snapshot:last_completed") in (None, "")
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
@@ -440,11 +512,12 @@ def test_run_recent_videos_refreshes_without_completion_markers():
|
|||||||
"""小时任务只刷新近窗新片,不写完成日 / 备份。"""
|
"""小时任务只刷新近窗新片,不写完成日 / 备份。"""
|
||||||
clock = 1_800_000_000
|
clock = 1_800_000_000
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
# view_for 须带与库内一致的 pubdate,否则快照回写会把近窗片挤出窗口
|
||||||
transport = SnapshotTransport({
|
transport = SnapshotTransport({
|
||||||
1: view_for(1, view=111),
|
1: view_for(1, view=111, pubdate=clock - 3600),
|
||||||
2: view_for(2, view=222),
|
2: view_for(2, view=222, pubdate=clock - 86400),
|
||||||
3: view_for(3, view=333),
|
3: view_for(3, view=333, pubdate=clock - 10 * 86400),
|
||||||
4: view_for(4, view=444),
|
4: view_for(4, view=444, pubdate=clock - 10 * 86400),
|
||||||
})
|
})
|
||||||
db, repo, service = make_service(tmp, transport, track_fans=())
|
db, repo, service = make_service(tmp, transport, track_fans=())
|
||||||
try:
|
try:
|
||||||
@@ -776,6 +849,8 @@ def main():
|
|||||||
test_snapshot_success_resume_and_force()
|
test_snapshot_success_resume_and_force()
|
||||||
test_resume_skips_ok_and_retries_failed()
|
test_resume_skips_ok_and_retries_failed()
|
||||||
test_unavailable_marks_removed()
|
test_unavailable_marks_removed()
|
||||||
|
test_snapshot_refreshes_video_metadata()
|
||||||
|
test_recent_snapshot_refreshes_metadata()
|
||||||
test_other_error_writes_failed_placeholder()
|
test_other_error_writes_failed_placeholder()
|
||||||
test_fan_failure_placeholder()
|
test_fan_failure_placeholder()
|
||||||
test_auth_invalid_is_warned_but_snapshot_continues()
|
test_auth_invalid_is_warned_but_snapshot_continues()
|
||||||
|
|||||||
@@ -68,13 +68,16 @@ def test_video_listing_filters_and_sorting():
|
|||||||
|
|
||||||
heat = q.list_videos(sort="heat")
|
heat = q.list_videos(sort="heat")
|
||||||
aids = [item["aid"] for item in heat["items"]]
|
aids = [item["aid"] for item in heat["items"]]
|
||||||
assert 104 not in aids, "removed 视频不应进入榜单"
|
# 热度榜仍按门槛过滤;104 removed 无成功快照,不进热度
|
||||||
|
assert 104 not in aids
|
||||||
# 综合热度榜应用最小播放门槛 2000:101(1200)与 103(120)被排除
|
# 综合热度榜应用最小播放门槛 2000:101(1200)与 103(120)被排除
|
||||||
assert aids == [102], aids
|
assert aids == [102], aids
|
||||||
assert heat["items"][0]["heat_percentile"] == 100.0
|
assert heat["items"][0]["heat_percentile"] == 100.0
|
||||||
|
|
||||||
views = q.list_videos(sort="view")
|
views = q.list_videos(sort="view")
|
||||||
assert [item["aid"] for item in views["items"]] == [102, 101, 103]
|
# removed(104) 站内继续展示;无指标排在末尾
|
||||||
|
assert [item["aid"] for item in views["items"]] == [102, 101, 103, 104]
|
||||||
|
assert views["items"][-1]["status"] == "removed"
|
||||||
|
|
||||||
interactions = q.list_videos(sort="interaction")
|
interactions = q.list_videos(sort="interaction")
|
||||||
assert interactions["items"][0]["aid"] == 101
|
assert interactions["items"][0]["aid"] == 101
|
||||||
@@ -91,14 +94,19 @@ def test_video_listing_filters_and_sorting():
|
|||||||
assert [item["aid"] for item in searched["items"]] == [102]
|
assert [item["aid"] for item in searched["items"]] == [102]
|
||||||
|
|
||||||
folder = q.list_videos(sort="view", folder_id=3700023351)
|
folder = q.list_videos(sort="view", folder_id=3700023351)
|
||||||
assert folder["total"] == 3
|
assert folder["total"] == 4 # 含 removed 104
|
||||||
folder = q.list_videos(sort="view", folder_id=999)
|
folder = q.list_videos(sort="view", folder_id=999)
|
||||||
assert folder["total"] == 0
|
assert folder["total"] == 0
|
||||||
|
|
||||||
page = q.list_videos(sort="view", page=2, page_size=1)
|
page = q.list_videos(sort="view", page=2, page_size=1)
|
||||||
assert page["total"] == 3
|
assert page["total"] == 4
|
||||||
assert page["pages"] == 3
|
assert page["pages"] == 4
|
||||||
assert [item["aid"] for item in page["items"]] == [101]
|
assert [item["aid"] for item in page["items"]] == [101]
|
||||||
|
|
||||||
|
detail = q.video_detail(104)
|
||||||
|
assert detail is not None
|
||||||
|
assert detail["status"] == "removed"
|
||||||
|
assert detail["aid"] == 104
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
@@ -457,7 +465,7 @@ def test_year_filter_and_published_sort():
|
|||||||
db, q = setup(tmp)
|
db, q = setup(tmp)
|
||||||
try:
|
try:
|
||||||
by_year = q.list_videos(sort="view", year=2026)
|
by_year = q.list_videos(sort="view", year=2026)
|
||||||
assert [item["aid"] for item in by_year["items"]] == [102, 101, 103]
|
assert [item["aid"] for item in by_year["items"]] == [102, 101, 103, 104]
|
||||||
assert by_year["year"] == 2026
|
assert by_year["year"] == 2026
|
||||||
|
|
||||||
none = q.list_videos(sort="view", year=2025)
|
none = q.list_videos(sort="view", year=2025)
|
||||||
@@ -465,10 +473,10 @@ def test_year_filter_and_published_sort():
|
|||||||
assert none["total"] == 0
|
assert none["total"] == 0
|
||||||
|
|
||||||
published = q.list_videos(sort="published")
|
published = q.list_videos(sort="published")
|
||||||
assert [item["aid"] for item in published["items"]] == [103, 101, 102]
|
assert [item["aid"] for item in published["items"]] == [103, 101, 102, 104]
|
||||||
|
|
||||||
added = q.list_videos(sort="added")
|
added = q.list_videos(sort="added")
|
||||||
assert added["total"] == 3
|
assert added["total"] == 4 # 含 removed
|
||||||
|
|
||||||
ups = q.list_ups(sort="efficiency", year=2026)
|
ups = q.list_ups(sort="efficiency", year=2026)
|
||||||
assert ups["year"] == 2026 and ups["total"] == 2
|
assert ups["year"] == 2026 and ups["total"] == 2
|
||||||
|
|||||||
Reference in new issue
Block a user