"""外部入口 id 回填:一位对上几个 id 就登记几条,已登记的跳过,撞号的点名等人判。""" import argparse import json import sqlite3 import tempfile import unittest from pathlib import Path from peach.entry_links import EXTERNAL_KIND from peach.migrations import upgrade from scripts.backfill_performer_entry_ids import ( CONFLICT, FC2_ONLY, HAVE, JAVDB, NAMESAKE, OK, TAKEN, apply_rows, plan, ) ROOT = Path(__file__).resolve().parents[1] MIGRATIONS = ROOT / "migrations" class EntryIdBackfillTests(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) self.root = Path(temporary.name).resolve() self.db = self.root / "ledger.db" sqlite3.connect(self.db).close() upgrade(self.db, MIGRATIONS) self.connection = sqlite3.connect(self.db) self.addCleanup(self.connection.close) for entity_id, name in ((10, "释爱丽丝"), (11, "天川空")): self.connection.execute( "INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at)" " VALUES(?,'performer',?,?,'t','t')", (entity_id, name, name)) self.connection.commit() self.cache = self.root / "javdb" self.cache.mkdir() def page(self, name: str, actor: str) -> None: """一页 javdb 资料页:名字在标题里,演员 id 在收藏那条链接里。""" (self.cache / f"{actor}.html").write_text( f'{name}' f'', encoding="utf-8") def search_page(self, stem: str, *cards: tuple[str, str, str]) -> None: """一张 javdb 搜索结果页:每张卡 (id, 标题一栏, 记录类型)。""" boxes = "".join( f'
' f'
' + (f'{record}' if record else "") + f"
{title}
" for actor, title, record in cards) (self.cache / f"{stem}.html").write_text( f'
{boxes}
', encoding="utf-8") def rows(self) -> list[dict]: args = argparse.Namespace(javdb_cache=self.cache, csv=[]) return plan(self.connection, args) def saved(self) -> list[tuple]: return self.connection.execute( "SELECT entity_id,external_id FROM entity_external_ref" " WHERE provider=? AND external_kind=? ORDER BY external_id", (JAVDB, EXTERNAL_KIND)).fetchall() def test_one_performer_with_two_pages_gets_both_registered(self): self.page("释爱丽丝", "d45k9") self.page("释爱丽丝", "ZX5z7") rows = self.rows() self.assertEqual([row["verdict"] for row in rows], [OK, OK]) self.assertEqual(apply_rows(self.connection, rows), (2, 2)) self.assertEqual(self.saved(), [(10, "ZX5z7"), (10, "d45k9")]) def test_the_record_kind_from_the_cached_cards_is_written_with_the_id(self): """搜索卡片认出的有碼/無碼一并记下;卡片上没有的 id 不猜(#271)。""" self.page("释爱丽丝", "d45k9") self.page("释爱丽丝", "ZX5z7") apply_rows(self.connection, self.rows(), {"d45k9": True}) metadata = dict(self.connection.execute( "SELECT external_id,metadata_json FROM entity_external_ref WHERE provider=?", (JAVDB,)).fetchall()) self.assertEqual(json.loads(metadata["d45k9"]), {"source": "backfill_performer_entry_ids", "uncensored": True}) self.assertEqual(json.loads(metadata["ZX5z7"]), {"source": "backfill_performer_entry_ids"}) def test_a_row_already_in_the_ledger_is_skipped_and_the_rest_still_land(self): """同一批证据可以重复跑:已登记的那条不报错、不重写、也不算冲突。""" self.page("释爱丽丝", "d45k9") apply_rows(self.connection, self.rows()) self.page("释爱丽丝", "ZX5z7") rows = self.rows() self.assertEqual([row["verdict"] for row in rows], [OK, HAVE]) self.assertEqual(apply_rows(self.connection, rows), (1, 1)) self.assertEqual(self.saved(), [(10, "ZX5z7"), (10, "d45k9")]) def test_an_id_held_by_another_performer_is_named_not_swallowed(self): """主键决定一个 id 只属于一位,所以这一条要写出占有者等人判。""" self.connection.execute( "INSERT INTO entity_external_ref(entity_id,provider,external_kind,external_id)" " VALUES(11,?,?,'d45k9')", (JAVDB, EXTERNAL_KIND)) self.connection.commit() self.page("释爱丽丝", "d45k9") rows = self.rows() self.assertEqual([row["verdict"] for row in rows], [TAKEN]) self.assertIn("11", rows[0]["evidence"]) self.assertEqual(apply_rows(self.connection, rows), (0, 0)) def test_two_performers_claiming_one_page_are_left_to_a_person(self): self.connection.execute( "INSERT INTO entity_alias(entity_id,alias,normalized_alias,source,confidence)" " VALUES(11,'释爱丽丝','释爱丽丝','test',1.0)") self.connection.commit() self.page("释爱丽丝", "d45k9") rows = self.rows() self.assertEqual([row["verdict"] for row in rows], [CONFLICT, CONFLICT]) self.assertEqual(apply_rows(self.connection, rows), (0, 0)) def test_japanese_glyphs_on_the_page_match_the_simplified_name(self): self.connection.execute( "INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at)" " VALUES(13,'performer','泽村玲子','泽村玲子','t','t')") self.connection.commit() self.page("澤村玲子", "d45k9") self.assertEqual([(row["entity_id"], row["verdict"]) for row in self.rows()], [(13, OK)]) def test_search_cards_give_both_records_of_one_performer(self): """资料页没取回时,搜索页的卡上就有 id;有碼、無碼各一张是同一个人。""" self.search_page("s1", ("d45k9", "释爱丽丝, 釈アリス", ""), ("ZX5z7", "释爱丽丝", "無碼"), ("Qq11", "天川そら", "")) rows = self.rows() self.assertEqual([(row["external_id"], row["verdict"]) for row in rows], [("ZX5z7", OK), ("d45k9", OK)]) def test_two_cards_of_the_same_record_kind_are_namesakes(self): self.search_page("s1", ("d45k9", "释爱丽丝", "")) self.search_page("s2", ("ZX5z7", "释爱丽丝", ""), ("d45k9", "释爱丽丝", "")) rows = self.rows() self.assertEqual([(row["verdict"], row["external_id"]) for row in rows], [(NAMESAKE, "")]) self.assertIn("ZX5z7、d45k9", rows[0]["evidence"]) self.assertEqual(apply_rows(self.connection, rows), (0, 0)) def test_a_short_single_name_on_a_card_is_not_an_anchor(self): self.connection.execute( "INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at)" " VALUES(12,'performer','葵','葵','t','t')") self.connection.commit() self.search_page("s1", ("Aa01", "葵", "")) self.assertEqual(self.rows(), []) def test_a_short_single_name_on_a_profile_page_is_not_an_anchor(self): self.connection.execute( "INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at)" " VALUES(12,'performer','葵','葵','t','t')") self.connection.commit() self.page("葵", "Aa01") self.assertEqual(self.rows(), []) def works(self, entity_id: int, *codes: str) -> None: for code in codes: asset = self.connection.execute( "INSERT INTO asset(location,path,name,medium,code) VALUES('local',?,?,'video',?)", (rf"R:\media\{code}.mp4", f"{code}.mp4", code)).lastrowid self.connection.execute( "INSERT INTO asset_entity(asset_id,entity_id,role,source,confidence)" " VALUES(?,?,'performer','test',1.0)", (asset, entity_id)) self.connection.commit() def test_an_fc2_creator_without_a_jav_directory_id_is_not_bound(self): """FC2 个人摄创作者不在 javdb 的人物收录里,名字对上的演员页是同名的另一个人。""" self.works(10, "FC2-PPV-1812235", "FC2-PPV-3202758") self.page("释爱丽丝", "d45k9") rows = self.rows() self.assertEqual([row["verdict"] for row in rows], [FC2_ONLY]) self.assertIn("2 部", rows[0]["evidence"]) self.assertEqual(apply_rows(self.connection, rows), (0, 0)) def test_fc2_works_do_not_hold_back_a_performer_known_to_a_jav_directory(self): self.works(10, "FC2-PPV-1812235") self.connection.execute( "INSERT INTO entity_external_ref(entity_id,provider,external_kind,external_id)" " VALUES(10,'minnano-av',?,'12345')", (EXTERNAL_KIND,)) self.connection.commit() self.page("释爱丽丝", "d45k9") self.assertEqual([row["verdict"] for row in self.rows()], [OK]) def test_one_studio_release_among_fc2_works_keeps_the_binding(self): self.works(10, "FC2-PPV-1812235", "SSIS-001") self.page("释爱丽丝", "d45k9") self.assertEqual([row["verdict"] for row in self.rows()], [OK]) if __name__ == "__main__": unittest.main()