"""K-MIB 官网解析与采集脚本的隔离测试。""" import importlib.util import sqlite3 import tempfile import unittest from pathlib import Path from peach import web_contract as rm_web from peach.catalog_rules import is_korean_mib_code, release_code_from_filename from peach.metadata_auto_apply import auto_apply_metadata from peach.genre_taxonomy import map_genres from peach.metadata_kmib import parse_list, parse_star, parse_video from peach.metadata_routes import classify from support.ledger import fresh_ledger SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "harvest_kmib.py" _spec = importlib.util.spec_from_file_location("harvest_kmib", SCRIPT) harvest = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(harvest) def video_page(code="AIN-203", actor="Ain", date="2024-11-20", runtime="00:31:03", categories=("Blowjob", "Sexy", "Kiss"), cover="/uploads/image/streaming/a.jpg"): tags = ", ".join(f"

{c}

" for c in categories) boxes = (("Title", "Do you wanna be my slave"), ("Introduction", "intro"), ("Release date", date), ("Playing time", runtime), ("Actor", actor), ("Code", code), ("Category", tags)) body = "".join(f'
{head}
' f'
{value}
' for head, value in boxes) return (f'
' f'
{body}') STAR_PAGE = """
Ain
adult movie actor
""" class KmibPageTests(unittest.TestCase): def test_video_page_yields_catalog_fields(self): release = parse_video(video_page(), 7) self.assertEqual(release["id"], "AIN-203") self.assertEqual(release["provider_id"], "AIN-203") self.assertEqual(release["source_url"], "https://www.k-mib.com/video/video-view.php?idx=7") self.assertEqual(release["release_date"], "2024-11-20") self.assertEqual(release["runtime"], 31.05) self.assertEqual(release["actresses"], [{"japanese_name": "Ain"}]) self.assertEqual((release["maker"], release["partner"]), ("MIB", False)) self.assertEqual(release["genres"], ["Blowjob", "Sexy", "Kiss"]) self.assertEqual(release["cover_url"], "https://www.k-mib.com/uploads/image/streaming/a.jpg") def test_placeholder_page_is_not_a_release(self): """没上架的 idx 各栏都空着,发行日期是 Unix 纪元。""" self.assertIsNone(parse_video(video_page(code="", date="1970-01-01"), 5)) def test_partner_label_in_actor_column_becomes_the_maker(self): release = parse_video(video_page(code="JS-012", actor="JS MEDIA"), 9) self.assertEqual((release["maker"], release["partner"]), ("JS MEDIA", True)) self.assertEqual(release["actresses"], []) def test_partner_prefix_keeps_its_real_cast(self): """`MMP-001` 的 Actor 栏写的是真人,厂牌只能从番号前缀认。""" release = parse_video(video_page(code="MMP-001", actor="Noah, Seoyeon"), 10) self.assertEqual(release["maker"], "MMP") self.assertEqual([a["japanese_name"] for a in release["actresses"]], ["Noah", "Seoyeon"]) def test_underscore_code_is_the_ledger_writing(self): self.assertEqual(parse_video(video_page(code="SOY_101"), 11)["id"], "SOY-101") def test_star_page_yields_name_portrait_and_profile(self): star = parse_star(STAR_PAGE, 3) self.assertEqual(star["name"], "Ain") self.assertEqual(star["image_url"], "https://www.k-mib.com/uploads/image/actor/ain.jpg") self.assertEqual(star["source_url"], "https://www.k-mib.com/star/star-view.php?idx=3") self.assertEqual((star["age"], star["bwh"]), ("24", "33-25-36")) self.assertEqual(star["tags"], ["Big Tits", "Slender"]) def test_list_page_separates_videos_and_stars(self): html = ("" "" "") self.assertEqual(parse_list(html), {"video": ["432"], "star": ["30"]}) class KmibCatalogRuleTests(unittest.TestCase): def test_mib_filenames_yield_their_codes(self): for name, code in ( ("AIN-203 Ain Training medical college girl.mp4", "AIN-203"), ("KJI-101 Ggulji Car sex with thick girl.mp4", "KJI-101"), ("[K-MIB]NOAH-101(NOAH)(1).mp4", "NOAH-101"), ): self.assertEqual(release_code_from_filename(name), code, name) def test_the_mib_filename_rule_stays_inside_mib_prefixes(self): # 下划线写法只对 MIB 前缀开放;「厂牌-序号 + 片名」的通用形态归片名规则。 self.assertEqual(release_code_from_filename("AIN_203 some title.mp4"), "AIN-203") self.assertIsNone(release_code_from_filename("ABC_123 some title.mp4")) self.assertIsNone(release_code_from_filename("MIB.mp4")) def test_official_catalogue_prefixes_are_korean_mib(self): for code in ("NOAH-101", "KJI-102", "MOSA-101", "SJSK-101"): self.assertTrue(is_korean_mib_code(code), code) # 合作厂牌不是 MIB 的命名体系。 self.assertFalse(is_korean_mib_code("STRC-001")) def test_attackers_yuj_is_not_a_mib_prefix(self): # `YUJ` 当初取自两部错名文件,实为官网 `AIN-101`、`AIN-102`(片名、片长逐一对上)。 # r18.dev 上 `YUJ-001`~`YUJ-080` 是 Attackers 的作品,`YUJ-072` 为 164 分钟。 self.assertFalse(is_korean_mib_code("YUJ-072")) self.assertEqual(classify("YUJ-072"), "censored") def test_kmib_categories_project_onto_existing_tags(self): """k-mib 的类目在来源页上就是英文,没有日文原词可换,只能靠表里收录英文写法。 投影仍然只落在既有词表上,门槛在 `test_genre_taxonomy`;`Sexy` 这类几乎每部 都挂的氛围词按非内容排除,不占一格标签。 """ self.assertEqual( map_genres(["Doggy-Style", "Masterbation", "Pretty Girl", "Sexy", "Kiss"]), (["后入", "自慰", "高颜值", "接吻"], [])) class KmibHarvestTests(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.root = Path(self.tmp.name) self.db = fresh_ledger(self.root) con = sqlite3.connect(self.db) for aid, code, name in ((1, "AIN-203", "AIN-203 Ain Training medical college girl.mp4"), (2, None, "[K-MIB]NOAH-101(NOAH)(1).mp4"), (3, None, "MIB.mp4"), (4, None, "NOAH-999 not listed.mp4"), (5, "AR-101", "AR-101 Ari.mp4")): con.execute("INSERT INTO asset(id,location,path,name,medium,code,size) " "VALUES(?,'local',?,?,'video',?,1000)", (aid, f"/x/{name}", name, code)) con.commit(); con.close() self.releases = [parse_video(video_page(), 7), parse_video(video_page(code="NOAH-101", actor="Noah"), 8), parse_video(video_page(code="JS-012", actor="JS MEDIA"), 9)] self.stars = [parse_star(STAR_PAGE, 3)] def tearDown(self): self.tmp.cleanup() def rows(self): con = sqlite3.connect(self.db) con.row_factory = sqlite3.Row try: return harvest.metadata_candidate_rows(con, self.releases, self.stars, fetched_at="2026-09-10T00:00:00Z") finally: con.close() def test_candidates_carry_their_own_item_keys_and_identity(self): """`<番号>:<字段>` 已被 JAV 错配候选的拒绝决定占着,官网候选要另起一个键。""" rows = self.rows() self.assertEqual({row["code"] for row in rows}, {"AIN-203"}) self.assertTrue(all(row["item_key"].endswith(":kmib") for row in rows)) by_field = {row["field"]: harvest.json.loads(row["candidates_json"])[0] for row in rows} self.assertEqual(set(by_field), {"title", "performers", "studio", "release_date", "tags"}) self.assertEqual(by_field["studio"]["value"], "MIB") self.assertEqual(by_field["performers"]["provider_id"], "AIN-203") self.assertEqual(by_field["performers"]["value"][0]["external_id"], "3") self.assertEqual(by_field["tags"]["value"], ["口交", "接吻"]) def test_missing_codes_come_from_filenames_listed_on_the_official_site(self): con = sqlite3.connect(self.db) try: filled = harvest.fill_missing_codes(con, {"AIN-203", "NOAH-101"}) con.commit() codes = dict(con.execute("SELECT id,code FROM asset")) finally: con.close() self.assertEqual(filled, [(2, "NOAH-101")]) self.assertEqual((codes[3], codes[4]), (None, None)) def test_ledger_codes_missing_from_the_site_are_not_obtained(self): con = sqlite3.connect(self.db) con.row_factory = sqlite3.Row try: rows = harvest.catalog_rows(con, self.releases) finally: con.close() results = {row["code"]: row["result"] for row in rows} self.assertEqual(results["AR-101"], "未取得") self.assertEqual(results["JS-012"], "取得") def test_auto_apply_fills_empty_fields_and_links_the_performer(self): candidates = self.root / "generated" candidates.mkdir() harvest.write_rows(candidates / harvest.CANDIDATE_FILE, harvest.METADATA_FIELDS, self.rows()) contract = rm_web.WebContract(Path(self.db), candidate_root=candidates, logo_root=self.root / "logos", avatar_root=self.root / "avatars") outcome = auto_apply_metadata(contract.database, candidates) # 标签这一行的 genre 全部有去向,所以和另外四个字段一样走补空落库; # 只要还剩一个未收录的词,`_tags_are_fully_resolved` 就把它交回人工。 self.assertEqual(outcome["applied"], 5) con = sqlite3.connect(self.db) try: asset = con.execute("SELECT catalog_title,release_date,studio FROM asset " "WHERE id=1").fetchone() tags = [r[0] for r in con.execute( "SELECT tag FROM asset_tag WHERE asset_id=1 AND tag NOT LIKE '演员:%' " "ORDER BY tag")] ref = con.execute( "SELECT e.canonical_name FROM entity_external_ref x JOIN entity e " "ON e.id=x.entity_id WHERE x.provider='kmib' AND x.external_kind='performer' " "AND x.external_id='3'").fetchone() finally: con.close() self.assertEqual(asset, ("Do you wanna be my slave", "2024-11-20", "MIB")) self.assertEqual(tags, ["口交", "接吻"]) self.assertEqual(ref, ("Ain",)) if __name__ == "__main__": unittest.main()