"""把一格装了多个艺名的别名拆成一人多名,以及不该被拆的四类括号。""" import sqlite3 import tempfile import unittest from pathlib import Path from peach.entities import split_composite_person_name from peach.metadata import normalized_performers, split_former_name from peach.metadata_auto_apply import _apply_performer_candidate from peach.migrations import upgrade from scripts.split_composite_aliases import ( CLEANUP_SOURCE, apply_review, apply_rows, collect, ) ROOT = Path(__file__).resolve().parents[1] class CompositePersonNameTests(unittest.TestCase): def test_a_packed_romaji_field_becomes_one_name_per_person(self): self.assertEqual( split_composite_person_name("Ako Momona (Kou Akemi, Mari Koizumi)"), ["Ako Momona", "Kou Akemi", "Mari Koizumi"]) self.assertEqual(split_composite_person_name("Nao (Hitomi Koike)"), ["Nao", "Hitomi Koike"]) def test_a_bracket_repeating_the_name_collapses_to_one(self): # 上游把括号无条件填满,`Rei Mizuna (Rei Mizuna)` 是模板产物不是两个人。 self.assertEqual(split_composite_person_name("Rei Mizuna (Rei Mizuna)"), ["Rei Mizuna"]) def test_names_carrying_dots_apostrophes_and_hyphens_still_split(self): self.assertEqual(split_composite_person_name("A.J. O'Neil (Mary-Ann Doe)"), ["A.J. O'Neil", "Mary-Ann Doe"]) def test_the_other_four_kinds_of_bracket_are_left_whole(self): # 括号里是厂牌消歧、角色出处、接稿状态、去重后缀——都不是这个人的另一个名字。 for name in ("AV DEBUT(本物人妻)", "アスナ(SAO)", "快慢扳机(接稿中)", "kitty(1)", "Mana(23)", "プレステージプレミアム(PRESTIGEPREMIUM)", "Egami(えがみ)", "Z(指定)ERO"): self.assertEqual(split_composite_person_name(name), [name]) def test_a_plain_name_comes_back_as_itself(self): self.assertEqual(split_composite_person_name(" 白石亚子 "), ["白石亚子"]) self.assertEqual(split_composite_person_name(""), []) class CompositeAliasSplitTests(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.db = Path(self.tmp.name).resolve() / "ledger.db" upgrade(self.db, ROOT / "migrations") self.con = sqlite3.connect(self.db) self.addCleanup(self.con.close) self.con.executemany( "INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at)" " VALUES(?,?,?,?,'t','t')", [(10, "performer", "藤木美夏", "藤木美夏"), (11, "performer", "里中静香", "里中静香"), (12, "series", "AV DEBUT(本物人妻)", "av debut(本物人妻)"), (13, "tag", "アスナ(SAO)", "アスナ(sao)"), (14, "performer", "Kou Akemi", "kou akemi"), (15, "performer", "新井爱丽", "新井爱丽")]) self.con.executemany( "INSERT INTO entity_alias(entity_id,alias,normalized_alias,source,confidence)" " VALUES(?,?,?,?,1.0)", [(10, "Ako Momona (Kou Akemi, Mari Koizumi)", "ako momona (kou akemi, mari koizumi)", "r18:performer"), (11, "Rei Mizuna (Rei Mizuna)", "rei mizuna (rei mizuna)", "r18:performer"), (12, "AV DEBUT (Real Married Woman)", "av debut (real married woman)", "r18dev:series-localization"), (15, "Elly Akira (Elly Arai)", "elly akira (elly arai)", "merge:duplicate-identity")]) self.con.commit() def _rows(self): return {str(row["value"]): row for row in collect(self.con)} def test_only_the_romaji_person_shape_is_marked_auto(self): rows = self._rows() self.assertEqual(rows["Elly Akira (Elly Arai)"]["verdict"], "auto") self.assertEqual(rows["Rei Mizuna (Rei Mizuna)"]["verdict"], "auto") # 系列别名的括号里是厂牌,标签的括号里是作品出处。 self.assertEqual(rows["AV DEBUT (Real Married Woman)"]["verdict"], "review") self.assertEqual(rows["AV DEBUT(本物人妻)"]["verdict"], "review") self.assertEqual(rows["アスナ(SAO)"]["verdict"], "review") def test_a_part_that_is_another_entitys_canonical_name_goes_to_review(self): # `Kou Akemi` 已经自己占了一条实体:那意味着可能要合并,不是拆别名顺手能定的。 row = self._rows()["Ako Momona (Kou Akemi, Mari Koizumi)"] self.assertEqual(row["verdict"], "review") self.assertIn("撞 entity 14", str(row["note"])) def test_applying_replaces_the_packed_row_with_one_row_per_name(self): moved = apply_rows(self.con, collect(self.con)) self.assertEqual(moved, {"added": 3, "removed": 2}) self.assertEqual( self.con.execute("SELECT alias,source FROM entity_alias WHERE entity_id=15" " ORDER BY alias").fetchall(), [("Elly Akira", "merge:duplicate-identity"), ("Elly Arai", "merge:duplicate-identity")]) # 每一段都记在提供它的那个来源名下:这几个罗马字确实是它给的,只是被打包在一个字段里。 self.assertEqual( self.con.execute("SELECT alias,source FROM entity_alias WHERE entity_id=11") .fetchall(), [("Rei Mizuna", "r18:performer")]) def test_applying_leaves_every_review_row_untouched(self): apply_rows(self.con, collect(self.con)) for entity_id, alias in ((10, "Ako Momona (Kou Akemi, Mari Koizumi)"), (12, "AV DEBUT (Real Married Woman)")): self.assertEqual( self.con.execute("SELECT count(*) FROM entity_alias" " WHERE entity_id=? AND alias=?", (entity_id, alias)).fetchone()[0], 1) self.assertEqual( self.con.execute("SELECT canonical_name FROM entity WHERE id=13") .fetchone()[0], "アスナ(SAO)") class ReviewedNameCleanupTests(unittest.TestCase): """形状分不开、只有人能判的那些:复核 CSV 填 `verdict` 与 `target` 再执行。""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.db = Path(self.tmp.name).resolve() / "ledger.db" upgrade(self.db, ROOT / "migrations") self.con = sqlite3.connect(self.db) self.addCleanup(self.con.close) self.con.executemany( "INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at)" " VALUES(?,?,?,?,'t','t')", [(20, "creator", "幼月月@一日目(土)東オ54a", "幼月月@一日目(土)東オ54a"), (21, "studio", "プレステージプレミアム", "プレステージプレミアム"), (22, "performer", "白石亚子", "白石亚子"), (23, "creator", "みどりふう", "みどりふう")]) self.con.execute( "INSERT INTO entity_alias(entity_id,alias,normalized_alias,source,confidence)" " VALUES(21,'プレステージプレミアム(PRESTIGEPREMIUM)'," "'プレステージプレミアム(prestigepremium)','javdb:studio',1.0)") self.con.executemany( "INSERT INTO asset(id,location,path,name,medium,creator)" " VALUES(?,'online',?,?,'image',?)", [(1, "pixiv/1", "a", "幼月月@一日目(土)東オ54a"), (2, "pixiv/2", "b", "幼月月@一日目(土)東オ54a"), (3, "pixiv/3", "c", "みどりふう")]) self.con.execute( "INSERT INTO asset_entity(asset_id,entity_id,role,source,confidence)" " VALUES(1,20,'creator','pixiv',1.0)") self.con.commit() def _row(self, verdict, entity_id, value, target, kind="creator", source=""): return {"verdict": verdict, "kind": kind, "entity_id": str(entity_id), "value": value, "target": target, "source": source} def test_stripping_a_booth_suffix_renames_the_entity_and_its_assets(self): done = apply_review(self.con, [ self._row("strip", 20, "幼月月@一日目(土)東オ54a", "幼月月")]) self.assertEqual((done["strip"], done["flat"]), (1, 2)) self.assertEqual( self.con.execute("SELECT canonical_name,normalized_name FROM entity" " WHERE id=20").fetchone(), ("幼月月", "幼月月")) # 扁平投影跟着改,否则卡片还写着旧名、按旧名照样搜得到。 self.assertEqual( sorted(name for (name,) in self.con.execute( "SELECT creator FROM asset WHERE creator IS NOT NULL")), ["みどりふう", "幼月月", "幼月月"]) def test_the_stripped_name_stays_as_an_alias_under_the_cleanup_source(self): apply_review(self.con, [self._row("strip", 20, "幼月月@一日目(土)東オ54a", "幼月月")]) self.assertEqual( self.con.execute("SELECT alias,source FROM entity_alias WHERE entity_id=20") .fetchall(), [("幼月月@一日目(土)東オ54a", CLEANUP_SOURCE)]) def test_splitting_a_studio_alias_keeps_the_source_that_gave_it(self): done = apply_review(self.con, [ self._row("split", 21, "プレステージプレミアム(PRESTIGEPREMIUM)", "プレステージプレミアム | PRESTIGEPREMIUM", kind="studio", source="javdb:studio")]) self.assertEqual(done["split"], 1) self.assertEqual( sorted(alias for (alias,) in self.con.execute( "SELECT alias FROM entity_alias WHERE entity_id=21")), ["PRESTIGEPREMIUM", "プレステージプレミアム"]) def test_a_target_taken_by_another_entity_is_refused_not_written(self): # 撞上另一条实体的规范名意味着两条可能该合并,那是合并要回答的问题。 done = apply_review(self.con, [ self._row("strip", 20, "幼月月@一日目(土)東オ54a", "みどりふう")]) self.assertEqual(done["strip"], 0) self.assertEqual(len(done["blocked"]), 1) self.assertIn("撞 entity 23", done["blocked"][0]) self.assertEqual( self.con.execute("SELECT canonical_name FROM entity WHERE id=20").fetchone()[0], "幼月月@一日目(土)東オ54a") def test_rows_left_at_review_do_nothing(self): done = apply_review(self.con, [ self._row("review", 20, "幼月月@一日目(土)東オ54a", "幼月月"), self._row("strip", 20, "幼月月@一日目(土)東オ54a", "")]) self.assertEqual((done["strip"], done["split"], done["flat"]), (0, 0, 0)) self.assertEqual( self.con.execute("SELECT canonical_name FROM entity WHERE id=20").fetchone()[0], "幼月月@一日目(土)東オ54a") def test_a_performer_strip_rewrites_the_flat_tag(self): self.con.execute( "INSERT INTO asset(id,location,path,name,medium)" " VALUES(4,'online','pixiv/4','d','image')") self.con.execute( "INSERT INTO asset_entity(asset_id,entity_id,role,source,confidence)" " VALUES(4,22,'performer','javdb',1.0)") self.con.execute( "INSERT INTO asset_tag(asset_id,tag,confidence,source)" " VALUES(4,'演员:白石亚子',1.0,'javdb')") done = apply_review(self.con, [ self._row("strip", 22, "白石亚子", "白石 あこ", kind="performer")]) self.assertEqual((done["strip"], done["flat"]), (1, 1)) self.assertEqual( [tag for (tag,) in self.con.execute( "SELECT tag FROM asset_tag WHERE asset_id=4")], ["演员:白石 あこ"]) class FormerNameTests(unittest.TestCase): """女优栏里的「现名(旧名)」:拆成一个人的两个名字,挂到账本里已有的那一位。""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.db = Path(self.tmp.name).resolve() / "ledger.db" upgrade(self.db, ROOT / "migrations") self.con = sqlite3.connect(self.db) self.con.row_factory = sqlite3.Row self.addCleanup(self.con.close) self.con.executemany( "INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at)" " VALUES(?,'performer',?,?,'t','t')", [(20, "美谷朱音", "美谷朱音"), (21, "姬川优奈", "姬川优奈")]) self.con.execute( "INSERT INTO entity_alias(entity_id,alias,normalized_alias,source,confidence)" " VALUES(21,'月城らん','月城らん','test',1.0)") self.con.execute( "INSERT INTO asset(id,location,path,name,medium,code,size)" " VALUES(1,'R:','R:\\media\\MIZD-997.mp4','MIZD-997.mp4','video','MIZD-997',1)") def land(self, raw_name: str, external_id: str = "") -> int: performers, _warnings = normalized_performers( [{"japanese_name": raw_name, "dmm_id": external_id, "profile_source": "r18dev"}]) _apply_performer_candidate(self.con, [1], {"value": performers}, source="r18dev", confidence=0.9, metadata={}, now="t") return self.con.execute( "SELECT entity_id FROM asset_entity WHERE asset_id=1 AND role='performer'" ).fetchone()[0] def test_current_and_former_names_split_apart(self): self.assertEqual(split_former_name("美谷朱音(美谷朱里)"), ("美谷朱音", "美谷朱里")) self.assertEqual(split_former_name("七海ひな(旧・七瀬ひな)"), ("七海ひな", "七瀬ひな")) performers, _warnings = normalized_performers([{"japanese_name": "美谷朱音(美谷朱里)"}]) self.assertEqual((performers[0]["name"], performers[0]["aliases"], performers[0]["former_names"]), ("美谷朱音", ["美谷朱里"], ["美谷朱里"])) def test_brackets_that_are_not_a_former_name_stay_whole(self): # 年龄、描述、罗马字、几个旧名挤在一格:都不是「一个现名配一个旧名」。 for name in ("Mana(23)", "みく(素人)", "Rei Mizuna (Rei Mizuna)", "美谷朱音(美谷朱里、朱里)", "ゆい(20歳)"): self.assertEqual(split_former_name(name), (name, "")) def test_the_current_name_lands_on_the_entity_already_there(self): self.assertEqual(self.land("美谷朱音(美谷朱里)", "1039982"), 20) self.assertEqual(self.con.execute( "SELECT count(*) FROM entity WHERE kind='performer'").fetchone()[0], 2) self.assertEqual([row[0] for row in self.con.execute( "SELECT tag FROM asset_tag WHERE asset_id=1")], ["演员:美谷朱音"]) def test_a_new_current_name_lands_on_whoever_holds_the_former_one(self): self.assertEqual(self.land("姫川ゆうな(月城らん)"), 21) self.assertEqual(self.con.execute( "SELECT count(*) FROM entity WHERE kind='performer'").fetchone()[0], 2) self.assertEqual(self.con.execute( "SELECT source FROM entity_alias WHERE entity_id=21 AND alias='姫川ゆうな'" ).fetchone()[0], "javinizer:current-name") if __name__ == "__main__": unittest.main()