tests/test_fc2_metadata.py
Rama master · réplica de lectura del repositorio original.
# -*- coding: utf-8 -*-
"""fc2cmadb 评论解析。样本取自 3312576 等页面上的真实评论。"""
from __future__ import annotations
import json
import sqlite3
import tempfile
import unittest
from pathlib import Path
from peach.sources.fc2cmadb import collection_parts, page_comments, parse_equivalences, parse_performers
from scripts.fetch_fc2_metadata import (
backfill,
harvest,
harvest_rows,
high_resolution_cover_url,
metadata_candidate_rows,
summarise,
translated_tags,
)
#: 站上评论是 CRLF 换行,样本照原样保留。
CRLF = "\r\n"
# 演员标记:全角空格分隔,一行可有多人。
PERFORMER_COMMENT = """2664942 ひかり
2355314 真夏
2802445 真夏
1955351 みなみ"""
MULTI_PERFORMER_COMMENT = """1934545 ゆう
2589532 未歩なな
2724256 未歩なな 皐月
*
1909413 すず"""
# 等价标记:断言拆成两行写,`=` 起头的行属于上一行。
SPLIT_EQUIV_COMMENT = """3312576-1
= 3090722-3
3312576-4
= 2471432 = 3090722-1 = 4605413"""
# 另一种写法:单行、下划线分片,末尾用 `bad:` 列对不上的分片。
INLINE_EQUIV_COMMENT = """3090722=3312576_1++
2471432=3312576_4
1912367=1909413=1952605=3312576_10
bad:
#2
#3312576-3=9"""
class ParsePerformersTest(unittest.TestCase):
def test_maps_video_id_to_names(self):
found = parse_performers(PERFORMER_COMMENT)
self.assertEqual(found["2355314"], ["真夏"])
self.assertEqual(found["1955351"], ["みなみ"])
def test_keeps_every_name_on_a_co_starred_line(self):
found = parse_performers(MULTI_PERFORMER_COMMENT)
self.assertEqual(found["2724256"], ["未歩なな", "皐月"])
def test_ignores_decorations_and_equivalence_lines(self):
found = parse_performers(MULTI_PERFORMER_COMMENT)
self.assertNotIn("*", found)
# 等价行里的数字不能被当成演员名读进来。
self.assertEqual(parse_performers(INLINE_EQUIV_COMMENT), {})
class ParseEquivalencesTest(unittest.TestCase):
def test_joins_a_continuation_line_onto_its_subject(self):
groups = parse_equivalences(SPLIT_EQUIV_COMMENT)
self.assertIn([("3312576", "1"), ("3090722", "3")], groups)
def test_reads_underscore_parts_and_long_chains(self):
groups = parse_equivalences(INLINE_EQUIV_COMMENT)
self.assertIn([("2471432", ""), ("3312576", "4")], groups)
self.assertIn(
[("1912367", ""), ("1909413", ""), ("1952605", ""), ("3312576", "10")],
groups,
)
def test_stops_at_the_bad_section(self):
"""`bad:` 之后是「对不上」的记录,读成等价会把错误关系写进候选。"""
groups = parse_equivalences(INLINE_EQUIV_COMMENT)
self.assertNotIn([("3312576", "3"), ("9", "")], groups)
for group in groups:
self.assertNotIn("9", [token[0] for token in group])
class CollectionTest(unittest.TestCase):
def test_counts_distinct_parts_of_this_id(self):
groups = parse_equivalences(SPLIT_EQUIV_COMMENT + "\n" + INLINE_EQUIV_COMMENT)
parts = collection_parts("3312576", groups)
self.assertEqual(parts["1"], "3090722")
self.assertEqual(parts["4"], "2471432")
self.assertGreaterEqual(len(parts), 3)
def test_a_standalone_work_is_not_a_collection(self):
groups = parse_equivalences("2471432 = 4605413")
self.assertEqual(collection_parts("2471432", groups), {})
class SummariseTest(unittest.TestCase):
def _props(self, comments, **article):
base = {"title": "作品", "image_url": "https://example.invalid/c.png",
"writer": {"name": "千華繚乱", "slug": "hyakka"}, "tags": []}
base.update(article)
return {"article": base, "comments": {"data": comments}}
def test_withholds_the_cover_from_a_collection(self):
"""合集封面套给每个分片,会让 21 段不同内容显示同一张图。"""
props = self._props([{"id": 1, "body": SPLIT_EQUIV_COMMENT},
{"id": 2, "body": INLINE_EQUIV_COMMENT}])
row = summarise("3312576", props)
self.assertEqual(row["is_collection"], "1")
self.assertEqual(row["cover_url"], "")
self.assertIn("封面不下发", row["note"])
def test_keeps_the_cover_for_a_single_work(self):
row = summarise("2355314", self._props([{"id": 1, "body": PERFORMER_COMMENT}]))
self.assertEqual(row["is_collection"], "")
self.assertEqual(row["cover_url"], "https://example.invalid/c.png")
def test_upgrades_fc2_listing_thumbnail_to_the_measured_1200px_rendition(self):
url = "https://contents-thumbnail2.fc2.com/w276/storage.example/cover.jpg"
self.assertEqual(high_resolution_cover_url(url),
"https://contents-thumbnail2.fc2.com/w1200/storage.example/cover.jpg")
def test_ranks_performers_by_how_many_comments_name_them(self):
props = self._props([{"id": 1, "body": "2355314 真夏"},
{"id": 2, "body": "2355314 真夏"},
{"id": 3, "body": "2355314 ひかり"}])
row = summarise("2355314", props)
self.assertEqual(row["performers"], "真夏 ひかり")
self.assertEqual(row["performer_votes"], "真夏:2 ひかり:1")
class MetadataCandidateTest(unittest.TestCase):
def test_translates_only_reviewed_fc2_tags_and_deduplicates_synonyms(self):
self.assertEqual(
translated_tags("素人 フェラ フェラチオ 口内発射 未知"),
["素人", "口交", "口爆"],
)
def test_article_title_tags_and_cover_evidence_enter_the_unified_review_shape(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
database = root / "ledger.db"
connection = sqlite3.connect(database)
connection.executescript("""
CREATE TABLE asset(
id INTEGER PRIMARY KEY,medium TEXT,code TEXT,size INTEGER,
catalog_title TEXT,disposal TEXT
);
CREATE TABLE asset_tag(asset_id INTEGER,tag TEXT);
CREATE TABLE genre_decision(
source_genre TEXT PRIMARY KEY,raw_genre TEXT NOT NULL,
peach_tag TEXT,decided_at TEXT NOT NULL
);
INSERT INTO asset VALUES(1,'video','FC2-PPV-3701252',1500000000,NULL,NULL);
""")
connection.commit(); connection.close()
source = root / "fc2-candidate-log.csv"
rows = metadata_candidate_rows([{
"code": "FC2 3701252", "video_id": "3701252", "result": "取得",
"title": "作品标题", "tags": "素人 フェラ 口内発射",
"duration": "46:44", "cover_url": "https://example.invalid/cover.jpg",
}], database, raw_snapshot=source, fetched_at="2026-08-19T00:00:00Z")
self.assertEqual({row["field"] for row in rows}, {"title", "tags"})
title = next(row for row in rows if row["field"] == "title")
candidate = json.loads(title["candidates_json"])[0]
self.assertEqual(candidate["provider"], "fc2cmadb")
self.assertEqual(candidate["source"], "fc2")
self.assertTrue(candidate["official"])
self.assertEqual(candidate["catalog_evidence"]["cover_url"]["value"],
"https://example.invalid/cover.jpg")
tag_row = next(row for row in rows if row["field"] == "tags")
tags = json.loads(tag_row["candidates_json"])[0]
self.assertEqual(tags["value"], ["素人", "口交", "口爆"])
class HarvestTest(unittest.TestCase):
"""评论是用户多年攒的人工经验,本地对不上的标记同样要留。"""
def _props(self, comments):
return {"article": {"title": "合集", "tags": []},
"comments": {"data": comments}}
def test_keeps_markings_for_video_ids_we_do_not_own(self):
collected = {}
harvest("3312576", self._props([{"id": 1, "body": MULTI_PERFORMER_COMMENT}]),
collected)
rows = {row["video_id"]: row for row in harvest_rows(collected, owned={"3312576"})}
# 库里没有 2724256,但它的演员标记必须留下来。
self.assertEqual(rows["2724256"]["performers"], "未歩なな 皐月")
self.assertEqual(rows["2724256"]["owned"], "")
self.assertEqual(rows["2724256"]["seen_on"], "3312576")
def test_records_which_page_a_marking_came_from(self):
collected = {}
harvest("3312576", self._props([{"id": 1, "body": PERFORMER_COMMENT}]), collected)
harvest("2355314", self._props([{"id": 2, "body": "2664942 ひかり"}]), collected)
rows = {row["video_id"]: row for row in harvest_rows(collected, owned={"2355314"})}
self.assertEqual(rows["2664942"]["seen_on"], "2355314 3312576")
self.assertEqual(rows["2664942"]["performer_votes"], "ひかり:2")
def test_equivalence_members_are_kept_both_ways(self):
collected = {}
harvest("3312576", self._props([{"id": 1, "body": SPLIT_EQUIV_COMMENT}]), collected)
rows = {row["video_id"]: row for row in harvest_rows(collected, owned=set())}
self.assertIn("3312576", rows["3090722"]["equivalents"].split())
self.assertIn("4605413", rows["2471432"]["equivalents"].split())
class RealWorldShapesTest(unittest.TestCase):
"""线上抓到的真实写法,照着记忆重画的正则读不出它们。"""
def test_reads_a_full_width_equals_sign(self):
"""日文输入法打出的是 `=`,只认半角等号会把这类断言整条丢掉。"""
groups = parse_equivalences("=2407240", subject="4143616")
self.assertEqual(groups, [[("4143616", ""), ("2407240", "")]])
def test_an_equals_line_without_a_subject_is_dropped(self):
self.assertEqual(parse_equivalences("=2407240"), [])
def test_reads_a_name_followed_by_work_links(self):
"""一行名字 + 若干作品链接,语义是同一个人的作品集。"""
body = CRLF.join(["剛毛マキちゃん",
"https://fc2ppvdb.com/articles/3701252",
"https://fc2ppvdb.com/articles/4078398"])
found = parse_performers(body)
self.assertEqual(found["3701252"], ["剛毛マキちゃん"])
self.assertEqual(found["4078398"], ["剛毛マキちゃん"])
def test_link_listing_without_a_single_name_is_not_guessed(self):
"""说明文字超过一行就说不准哪行是名字,宁可不认。"""
body = CRLF.join(["同一人?", "たぶん",
"https://fc2ppvdb.com/articles/3701252"])
self.assertEqual(parse_performers(body), {})
class BackfillTest(unittest.TestCase):
def test_takes_performers_marked_on_another_works_page(self):
"""`4176112` 页上那条链接列表同时认领了 `3701252`,候选行得吃到。"""
collected = {}
body = CRLF.join(["剛毛マキちゃん",
"https://fc2ppvdb.com/articles/3701252"])
harvest("4176112", {"comments": {"data": [{"id": 1, "body": body}]}}, collected)
rows = backfill([{"video_id": "3701252", "performers": "",
"performer_votes": "", "equivalents": ""}], collected)
self.assertEqual(rows[0]["performers"], "剛毛マキちゃん")
def test_keeps_equivalents_found_on_the_works_own_page(self):
collected = {}
harvest("4143616", {"comments": {"data": [{"id": 1, "body": "=2407240"}]}},
collected)
rows = backfill([{"video_id": "4143616", "performers": "",
"performer_votes": "", "equivalents": "9999999"}], collected)
self.assertEqual(rows[0]["equivalents"], "2407240 9999999")
class PageCommentsTest(unittest.TestCase):
def test_deduplicates_the_two_overlapping_comment_lists(self):
"""`comments.data` 与 `article.comments` 重叠,相加会让一条评论投两票。"""
props = {"comments": {"data": [{"id": 7, "body": "2355314 真夏"}]},
"article": {"comments": [{"id": 7, "body": "2355314 真夏"},
{"id": 8, "body": "2355314 ひかり"}]}}
self.assertEqual(len(page_comments(props)), 2)
self.assertEqual(summarise("2355314", props)["performer_votes"],
"ひかり:1 真夏:1")
if __name__ == "__main__":
unittest.main()