feat(web): up avatars, follower count, and dedupe views/avg secondary

This commit is contained in:
kairee committed 2026-11-01 14:34:23 +08:00
1 parent 1ed3746f4b
commit 2ff3e7952b
23 files changed
+209 -58

No files matched your search

+1 -1
View File
@@ -182,7 +182,7 @@ VIEW_PAYLOAD: dict[str, Any] = {
{"cid": 43184885058, "page": 1, "from": "vupload", "part": "第一部分"},
{"cid": 43184885059, "page": 2, "from": "vupload", "part": "第二部分"},
],
"owner": {"mid": 434442072, "name": "_-抱抱-_", "face": ""},
"owner": {"mid": 434442072, "name": "_-抱抱-_", "face": "http://i1.hdslb.com/bfs/face/example.jpg"},
"stat": {
"aid": 117530286889638,
"view": 819,
+2
View File
@@ -353,6 +353,7 @@ def test_view_detail_by_aid_and_bvid():
assert detail.pages == 2
assert detail.cover and detail.cover.startswith("http")
assert detail.metrics()["view"] == 819
assert detail.up_face and detail.up_face.startswith("http")
assert "aid=117530286889638" in transport.calls[0]["url"]
transport = QueueTransport([http_json(VIEW_PAYLOAD)])
@@ -373,6 +374,7 @@ def test_view_detail_fallbacks_and_required_arg():
assert detail.duration == 0
assert detail.pages is None
assert detail.cover is None
assert detail.up_face is None
client, _ = make_client(QueueTransport([]))
for kwargs in ({}, {"aid": 1, "bvid": "BV1xvxi6LEZD"}):
+1 -1
View File
@@ -51,7 +51,7 @@ def test_init_and_status():
path = write_config(tmp)
code, output = run_cli(["init", "--config", str(path)])
assert code == 0
assert '"schema_version": 5' in output
assert '"schema_version": 6' in output
db = Database(Path(tmp) / "app.db")
db.initialize()
+4 -4
View File
@@ -24,7 +24,7 @@ def test_write_read_transaction():
with tempfile.TemporaryDirectory() as tmp:
db = make_db(tmp)
try:
assert db.schema_version == 5
assert db.schema_version == 6
db.write(lambda c: c.execute("INSERT INTO kv(k, v) VALUES ('a', '1')"))
assert db.read(lambda c: c.execute("SELECT v FROM kv WHERE k='a'").fetchone()[0]) == "1"
@@ -94,8 +94,8 @@ def test_close_and_reinitialize():
db2 = make_db(tmp)
try:
assert db2.schema_version == 5
assert db2.read(lambda c: c.execute("SELECT MAX(version) FROM schema_version").fetchone()[0]) == 5
assert db2.schema_version == 6
assert db2.read(lambda c: c.execute("SELECT MAX(version) FROM schema_version").fetchone()[0]) == 6
finally:
db2.close()
@@ -123,7 +123,7 @@ def test_migration_backup_on_existing_db():
db = Database(path, backup_dir=Path(tmp) / "backup")
try:
applied = db.initialize()
assert applied == [1, 2, 3, 4, 5]
assert applied == [1, 2, 3, 4, 5, 6]
assert len(list((Path(tmp) / "backup").glob("app-pre-v0-*.db"))) == 1
finally:
db.close()
+2 -1
View File
@@ -45,7 +45,7 @@ def detail(
data["pubdate"] = pubdate
data["ctime"] = pubdate
data["duration"] = duration
data["owner"] = {"mid": up_mid, "name": up_name, "face": ""}
data["owner"] = {"mid": up_mid, "name": up_name, "face": "http://i1.hdslb.com/bfs/face/u.jpg"}
data["pages"] = [{"cid": 1, "page": i + 1} for i in range(pages)]
return VideoDetail.from_api(data)
@@ -150,6 +150,7 @@ def test_ingest_candidate_added_and_snapshot():
assert video["source"] == "candidate:telegram"
assert video["folder_id"] is None
assert video["up_mid"] == 100
assert video["up_face"] == "http://i1.hdslb.com/bfs/face/u.jpg"
snap = repo.get_snapshot(DATE, 9)
assert snap is not None and snap["ok"] == 1 and snap["view"] == 819
assert ups.calls == [([100], {100: "UP100"})]
+9 -9
View File
@@ -37,8 +37,8 @@ def test_fresh_and_idempotent():
conn = sqlite3.connect(Path(tmp) / "app.db")
try:
applied = run_migrations(conn)
assert applied == [1, 2, 3, 4, 5]
assert current_version(conn) == 5
assert applied == [1, 2, 3, 4, 5, 6]
assert current_version(conn) == 6
assert EXPECTED_TABLES <= table_names(conn)
assert "schema_version" in table_names(conn)
columns = {
@@ -54,7 +54,7 @@ def test_fresh_and_idempotent():
}
assert {
"pages", "duration", "up_follower", "up_follower_at",
"up_follower_backfilled",
"up_follower_backfilled", "up_face",
} <= video_columns
profile_columns = {
row[1] for row in conn.execute("PRAGMA table_info(up_profile)")
@@ -66,7 +66,7 @@ def test_fresh_and_idempotent():
# 再跑一次不应重复应用
assert run_migrations(conn) == []
row = conn.execute("SELECT COUNT(*) FROM schema_version").fetchone()
assert row[0] == 5
assert row[0] == 6
finally:
conn.close()
@@ -81,15 +81,15 @@ def test_backup_before_upgrade():
conn.execute("INSERT INTO kv(k, v) VALUES ('hello', 'world')")
conn.commit()
v6 = Migration(6, "add probe", ("CREATE TABLE probe (id INTEGER PRIMARY KEY)",))
v7 = Migration(7, "add probe", ("CREATE TABLE probe (id INTEGER PRIMARY KEY)",))
applied = run_migrations(
conn,
backup_dir=backup_dir,
now=1_800_000_000,
migrations=MIGRATIONS + (v6,),
migrations=MIGRATIONS + (v7,),
)
assert applied == [6]
backups = list(backup_dir.glob("app-pre-v5-*.db"))
assert applied == [7]
backups = list(backup_dir.glob("app-pre-v6-*.db"))
assert len(backups) == 1
check = sqlite3.connect(backups[0])
try:
@@ -174,7 +174,7 @@ def test_v5_normalizes_unfaved():
)
conn.commit()
applied = run_migrations(conn)
assert applied == [5]
assert applied == [5, 6]
rows = dict(conn.execute("SELECT aid, status FROM video").fetchall())
assert rows == {1: "active", 2: "dropped"}
finally:
+3
View File
@@ -153,6 +153,9 @@ def test_snapshot_success_resume_and_force():
assert (row["view"], row["like"], row["favorite"], row["coin"],
row["share"], row["reply"], row["danmaku"]) == (100, 10, 3, 2, 1, 4, 5)
assert row["ok"] == 1 and row["src"] == "view"
assert repo.get_video(1)["up_face"] == (
"http://i1.hdslb.com/bfs/face/example.jpg"
)
fan = repo.get_up_stat(DATE, 37754047)
assert fan["follower"] == 2767268 and fan["ok"] == 1
assert repo.kv_get(service.completed_key(DATE)) == "1"
+6 -3
View File
@@ -182,6 +182,8 @@ def test_up_queries():
assert ups["items"][0]["video_count"] == 2
assert ups["items"][0]["total_views"] == 6400
assert ups["items"][0]["avg_views"] == 3200.0
assert ups["items"][0]["up_face"] == "https://i0.hdslb.com/bfs/face/a.jpg"
assert ups["items"][0]["follower_latest"] == 12345
one = q.list_ups(min_videos=2)
assert one["total"] == 1
@@ -190,6 +192,7 @@ def test_up_queries():
assert detail["video_count"] == 2
assert detail["total_views"] == 6400
assert detail["blacklisted"] is False
assert detail["up_face"] == "https://i0.hdslb.com/bfs/face/a.jpg"
assert detail["series"]["dates"] == list(DATES)
assert detail["series"]["views"] == [6000, 6200, 6400]
assert detail["space_url"].endswith("/1")
@@ -454,7 +457,7 @@ def test_about_payload():
assert about["snapshot_days"] == 3
assert about["snapshot_first_date"] == DATES[0]
assert about["fans_start_date"] == DATES[0]
assert about["schema_version"] == 5
assert about["schema_version"] == 6
assert about["analytics"] == {
"min_duration_seconds": 600,
"maturity_days": 2,
@@ -464,8 +467,8 @@ def test_about_payload():
}
assert about["heat_weights"]["view"] == 0.30
assert about["followers"]["videos_total"] == 3
assert about["followers"]["videos_covered"] == 0
assert about["followers"]["videos_missing"] == 3
assert about["followers"]["videos_covered"] == 3
assert about["followers"]["videos_missing"] == 0
assert about["folders"][0]["media_id"] == 3700023351
assert about["preview"] is False
finally:
+1 -1
View File
@@ -75,7 +75,7 @@ def test_readonly_routes_and_static():
assert status == 200 and payload["ok"] is True
status, _, payload = running.json("/api/about")
assert status == 200
assert payload["data"]["schema_version"] == 5
assert payload["data"]["schema_version"] == 6
assert payload["data"]["analytics"]["min_duration_seconds"] == 600
status, _, overview = running.json("/api/overview")
+10
View File
@@ -106,6 +106,16 @@ def build_db(tmp: str | Path) -> Path:
)
repo.set_video_status(103, "unfaved")
# UP 头像与最新粉丝数(UP 列表/详情展示用)
repo.apply_up_follower(
mid=1, name="UP A", follower=12345, following=5, now=ts("2026-10-30")
)
repo.apply_up_follower(
mid=2, name="UP B", follower=678, following=1, now=ts("2026-10-31")
)
repo.apply_video_up_face(101, "//i0.hdslb.com/bfs/face/a.jpg")
repo.apply_video_up_face(103, "//i0.hdslb.com/bfs/face/b.jpg")
metrics = {
101: {"view": 1000, "like": 100, "coin": 20, "favorite": 30, "share": 2, "reply": 10, "danmaku": 12},
102: {"view": 5000, "like": 200, "coin": 50, "favorite": 100, "share": 10, "reply": 30, "danmaku": 40},