Ctrl K
ComunidadGuías…

tests/test_code_creators.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import importlib.util
import json
import sqlite3
import sys
import tempfile
import unittest
from unittest import mock
from pathlib import Path


SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"


def _load(name):
    spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py")
    module = importlib.util.module_from_spec(spec)
    sys.modules[name] = module
    spec.loader.exec_module(module)
    return module


_script = _load("audit_code_creators")
confirm = _load("confirm_code_dirs")


class _Audit:
    """判定与清理归 `peach.code_creators`,命令行入口留在脚本里;测试两边都要够得着。"""
    from peach.catalog_rules import release_code_from_filename as code_from_filename
    from peach.catalog_rules import release_code_from_text as canonical_code
    from peach.code_creators import (  # noqa: F401  逐个点名,测试不跟着模块表面漂移
        FIELDS, VERDICT_CODE, VERDICT_KEEP, VERDICT_SITE, VERDICT_UNCLEAR,
        apply_rows, classify, collect, is_filesystem_path,
    )
    build_parser = staticmethod(_script.build_parser)
    run = staticmethod(_script.run)
    main = staticmethod(_script.main)


audit = _Audit


SCHEMA = """
CREATE TABLE asset(
  id INTEGER PRIMARY KEY, location TEXT NOT NULL, path TEXT NOT NULL, name TEXT,
  medium TEXT, creator TEXT, code TEXT, duration REAL, field_owners TEXT,
  mutation_revision INTEGER NOT NULL DEFAULT 0, UNIQUE(location,path));
CREATE TABLE entity(
  id INTEGER PRIMARY KEY, kind TEXT, canonical_name TEXT, normalized_name TEXT,
  metadata_json TEXT DEFAULT '{}', created_at TEXT, updated_at TEXT,
  UNIQUE(kind,normalized_name));
CREATE TABLE asset_entity(
  asset_id INTEGER, entity_id INTEGER, role TEXT, source TEXT, confidence REAL DEFAULT 1.0,
  metadata_json TEXT DEFAULT '{}', first_seen_at TEXT, last_seen_at TEXT,
  UNIQUE(asset_id,entity_id,role,source));
"""


class CodeShapeTests(unittest.TestCase):
    def test_digitless_creator_names_do_not_parse_their_files_as_embedded_codes(self):
        from peach import code_creators
        assets = [{'path': f'/media/{index}.mp4', 'name': f'ABP-{index:03d}.mp4', 'code': ''}
                  for index in range(1000)]
        with mock.patch.object(code_creators, 'code_from_filename', side_effect=AssertionError('无需解析影片名')):
            self.assertEqual(code_creators.classify('Example Creator', assets)[0], audit.VERDICT_KEEP)

    def test_quality_prefix_is_not_part_of_the_code(self):
        # 这批目录名正是本次要修的:HD 是画质,不是番号的一部分。
        self.assertEqual(audit.canonical_code("HD-abp-758"), "ABP-758")
        self.assertEqual(audit.canonical_code("HD_MIZD-997"), "MIZD-997")
        self.assertEqual(audit.canonical_code("HD_hrv-041"), "HRV-041")
        self.assertEqual(audit.canonical_code("FHD-ssis-070"), "SSIS-070")

    def test_version_suffix_is_stripped(self):
        self.assertEqual(audit.canonical_code("pppd-937ch"), "PPPD-937")
        self.assertEqual(audit.canonical_code("KBI044C"), "KBI-044")
        self.assertEqual(audit.canonical_code("NHDTB500C"), "NHDTB-500")

    def test_digits_are_normalised_to_three_places(self):
        self.assertEqual(audit.canonical_code("abw104"), "ABW-104")
        self.assertEqual(audit.canonical_code("IPVR00296"), "IPVR-296")

    def test_date_code_systems(self):
        # 分隔符是片商标识:加勒比写 `-`、一本道写 `_`,同日同序号是两部不同影片,
        # 目录名给的是哪一个就留哪一个。
        self.assertEqual(audit.canonical_code("Carib-040221-001-FHD"), "040221-001")
        self.assertEqual(audit.canonical_code("1pondo-071213_625-HD"), "071213_625")

    def test_names_that_are_not_codes(self):
        for name in ("涼森れむ", "suzuq", "Timepasserby", "MyElla"):
            self.assertIsNone(audit.canonical_code(name), name)

    def test_uuid_first_segment_is_not_a_code(self):
        self.assertIsNone(
            audit.canonical_code("DCE7230C-730E-45DA-9FDC-D167663BC84E"))

    def test_filename_noise_is_stripped_before_matching(self):
        self.assertEqual(audit.code_from_filename("HD-abp-758.mp4"), "ABP-758")
        self.assertEqual(audit.code_from_filename("HD_hrv-041-1.mp4"), "HRV-041")
        self.assertEqual(audit.code_from_filename("BNST033(2).jpg"), "BNST-033")
        self.assertEqual(audit.code_from_filename("040221-001-carib-1080p.mp4"), "040221-001")

    def test_online_identities_are_not_filesystem_paths(self):
        self.assertFalse(audit.is_filesystem_path("https://www.pixiv.net/users/93812377"))
        self.assertTrue(audit.is_filesystem_path(r"B:\云下载\HD-abp-758\HD-abp-758.mp4"))


def _row(name, path, code=None):
    return {"name": name, "path": path, "code": code}


class ClassifyTests(unittest.TestCase):
    def test_release_folder_named_after_its_own_code(self):
        verdict, identity, _ = audit.classify(
            "HD-abp-758", [_row("HD-abp-758.mp4", r"B:\云下载\HD-abp-758\HD-abp-758.mp4")])
        self.assertEqual((verdict, identity), (audit.VERDICT_CODE, "ABP-758"))

    def test_existing_code_column_counts_as_evidence(self):
        verdict, identity, _ = audit.classify(
            "pppd-937ch",
            [_row("PPPD-937CH.mp4", r"B:\云下载\pppd-937ch\PPPD-937CH.mp4", "PPPD-937")])
        self.assertEqual((verdict, identity), (audit.VERDICT_CODE, "PPPD-937"))

    def test_uploader_account_with_title_filenames_is_not_touched(self):
        # banbi_555 形状上像番号,但目录里全是作品标题,没有同番号文件。
        verdict, _, _ = audit.classify("banbi_555", [
            _row("18歳Eカップ彼氏持ち美女.mp4", r"A:\Pack From Shared\pen\banbi_555\18歳.mp4"),
            _row("2_2024_06_08_172104.mp4", r"A:\Pack From Shared\pen\banbi_555\2_2024.mp4"),
        ])
        self.assertEqual(verdict, audit.VERDICT_UNCLEAR)

    def test_a_download_site_folder_around_the_code_is_still_a_release_folder(self):
        """站名、画质和分享标记贴在番号前后,目录仍是发行目录,不是创作者。

        本机实测的写法:`Jav.li_MIAD573_HD`、`[98t.tv][98t.tv]ABW-251`、
        `nes@第一会所@ATID-479`、`kpxvs-300MIUM-698`。整名比对认不出它们,而 11 个这样的
        「创作者」名下挂着 33 条资产,在创作者索引里各占一个假身份。
        """
        for name, sample, code, identity in (
            ("Jav.li_MIAD573_HD", "MIAD573_01.wmv", "MIAD-573", "MIAD-573"),
            ("[98t.tv][98t.tv]ABW-251", "ABW-251.mp4", "", "ABW-251"),
            ("nes@第一会所@ATID-479", "ATID-479.mp4", "", "ATID-479"),
        ):
            with self.subTest(name=name):
                verdict, found, _ = audit.classify(
                    name, [_row(sample, rf"B:\云下载\{name}\{sample}", code)])
                self.assertEqual((verdict, found), (audit.VERDICT_CODE, identity))

    def test_an_uploader_account_shaped_like_a_code_is_not_matched_by_containment(self):
        """按包含关系找番号时,上传者账号是最容易被误伤的一类。

        `banbi_555` 的 code 列存的就是目录名 `BANBI_555`,紧凑形一比自然「包含」。
        发行番号这道形态门槛把它挡在外面,所以它仍然只是存疑,留给人看。
        """
        verdict, _, _ = audit.classify("banbi_555", [
            _row("18歳Eカップ彼氏持ち美女.mp4",
                 r"A:\Pack From Shared\pen\banbi_555\18歳.mp4", "BANBI_555"),
        ])
        self.assertEqual(verdict, audit.VERDICT_UNCLEAR)

    def test_pixiv_artist_is_kept_out_of_the_review_entirely(self):
        verdict, _, _ = audit.classify(
            "AH18", [_row("AH18", "https://www.pixiv.net/users/93812377")])
        self.assertEqual(verdict, audit.VERDICT_KEEP)

    def test_site_post_id_is_classified_apart_from_codes(self):
        verdict, identity, _ = audit.classify("fantia-3760310", [
            _row("avruby.png", r"B:\xxr\fantia-3760310\avruby.png"),
            _row("fantia-3760310.mp4", r"B:\xxr\fantia-3760310\fantia-3760310.mp4"),
        ])
        self.assertEqual((verdict, identity), (audit.VERDICT_SITE, "fantia-3760310"))


class ApplyTests(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.addCleanup(self.tmp.cleanup)
        self.db = Path(self.tmp.name) / "ledger.db"
        self.connection = sqlite3.connect(self.db)
        self.addCleanup(self.connection.close)
        self.connection.executescript(SCHEMA)

    # 媒介按扩展名给,和真实导入一致:目录里混着图片正是本组要覆盖的形态。
    def _creator(self, entity_id, name, assets):
        self.connection.execute(
            "INSERT INTO entity(id,kind,canonical_name,normalized_name) VALUES(?,'creator',?,?)",
            (entity_id, name, name.casefold()))
        for asset_id, asset_name, path, code in assets:
            self.connection.execute(
                "INSERT INTO asset(id,location,path,name,medium,creator,code) "
                "VALUES(?,'local',?,?,?,?,?)",
                (asset_id, path, asset_name,
                 "image" if asset_name.lower().endswith((".jpg", ".png")) else "video",
                 name, code))
            self.connection.execute(
                "INSERT INTO asset_entity(asset_id,entity_id,role,source) "
                "VALUES(?,?,'creator','legacy:asset')", (asset_id, entity_id))

    def test_apply_removes_the_false_creator_and_fills_the_code(self):
        self._creator(1, "HD-abp-758", [
            (10, "HD-abp-758.mp4", r"B:\云下载\HD-abp-758\HD-abp-758.mp4", None)])
        self._creator(2, "banbi_555", [
            (20, "18歳.mp4", r"A:\Pack From Shared\pen\banbi_555\18歳.mp4", None)])
        self.connection.commit()

        rows = audit.collect(self.connection)
        counts = audit.apply_rows(self.connection, rows)

        self.assertEqual(counts["links"], 1)
        self.assertEqual(counts["entities"], 1)
        self.assertEqual(counts["codes"], 1)
        self.assertEqual(
            self.connection.execute("SELECT creator,code FROM asset WHERE id=10").fetchone(),
            (None, "ABP-758"))
        self.assertIsNone(
            self.connection.execute("SELECT id FROM entity WHERE id=1").fetchone())

        # 存疑的上传者账号必须原样保留:实体、关系和扁平字段都不动。
        self.assertIsNotNone(
            self.connection.execute("SELECT id FROM entity WHERE id=2").fetchone())
        self.assertEqual(
            self.connection.execute("SELECT creator FROM asset WHERE id=20").fetchone()[0],
            "banbi_555")

    def test_the_code_it_writes_names_its_owner(self):
        self._creator(6, "HD-abp-758", [
            (60, "HD-abp-758.mp4", r"B:\云下载\HD-abp-758\HD-abp-758.mp4", None)])
        self.connection.commit()
        audit.apply_rows(self.connection, audit.collect(self.connection))
        self.assertEqual(
            json.loads(self.connection.execute(
                "SELECT field_owners FROM asset WHERE id=60").fetchone()[0]),
            {"code": "script:code-creators", "creator": "script:code-creators"})

    def test_a_code_the_user_cleared_stays_cleared(self):
        # 用户把某条的 code 清空是一次判断,补空不该把它再填回去。
        self._creator(7, "Tokyo-Hot n0781-HD", [
            (70, "Tokyo-Hot.mp4", r"B:\云下载\Tokyo-Hot n0781-HD\Tokyo-Hot.mp4", "n0781"),
            (71, "花絮.mp4", r"B:\云下载\Tokyo-Hot n0781-HD\花絮.mp4", None)])
        self.connection.execute(
            """UPDATE asset SET field_owners='{"code":"user:manual"}' WHERE id=71""")
        self.connection.commit()

        counts = audit.apply_rows(self.connection, audit.collect(self.connection))

        self.assertEqual(counts["codes"], 0)
        self.assertIsNone(
            self.connection.execute("SELECT code FROM asset WHERE id=71").fetchone()[0])

    def test_existing_code_is_never_overwritten(self):
        self._creator(3, "MIDA-117ch", [
            (30, "MIDA-117CH.mp4", r"B:\云下载\MIDA-117ch\MIDA-117CH.mp4", "MIDA-117")])
        self.connection.commit()
        audit.apply_rows(self.connection, audit.collect(self.connection))
        self.assertEqual(
            self.connection.execute("SELECT code FROM asset WHERE id=30").fetchone()[0],
            "MIDA-117")

    def test_site_post_id_clears_the_creator_without_writing_a_code(self):
        self._creator(4, "fantia-3760310", [
            (40, "fantia-3760310.mp4", r"B:\xxr\fantia-3760310\fantia-3760310.mp4", None)])
        self.connection.commit()
        audit.apply_rows(self.connection, audit.collect(self.connection))
        self.assertEqual(
            self.connection.execute("SELECT creator,code FROM asset WHERE id=40").fetchone(),
            (None, None))

    def test_apply_refuses_to_run_without_a_backup(self):
        parser = audit.build_parser()
        with self.assertRaises(SystemExit):
            audit.run(parser.parse_args(["--db", str(self.db), "--apply"]))


class DurationEvidenceTests(unittest.TestCase):
    """目录名像番号、文件却不带番号时,片长是第二条证据(用户 2026-09-16)。"""

    def test_a_couple_of_minutes_of_ads_still_counts_as_the_same_release(self):
        confirmed, _ = confirm.duration_confirms_code(98 * 60 + 90, 98)
        self.assertTrue(confirmed)

    def test_a_different_length_is_a_different_thing(self):
        # `bbsxv.xyz-DOCP-324` 目录里只有一条 90 秒的广告,DOCP-324 本身 120 分钟。
        confirmed, reason = confirm.duration_confirms_code(90, 120)
        self.assertFalse(confirmed)
        self.assertIn("超出容差", reason)

    def test_no_runtime_from_the_source_decides_nothing(self):
        self.assertEqual(confirm.duration_confirms_code(3600, None),
                         (False, "来源没有给片长"))

    def test_the_longest_video_is_the_feature(self):
        """目录里还躺着论坛文宣和封面图,拿它们比片长必然对不上。"""
        assets = [
            {"medium": "video", "duration": 5896.8},
            {"medium": "video", "duration": 31.0},
            {"medium": "image", "duration": None},
        ]
        self.assertEqual(confirm.main_video_seconds(assets), 5896.8)
        self.assertIsNone(confirm.main_video_seconds([{"medium": "image", "duration": None}]))


class ConfirmScriptTests(unittest.TestCase):
    class _Provider:
        """按番号答片长的假来源。"""

        def __init__(self, runtimes):
            self.runtimes, self.asked = runtimes, []

        def query(self, code, **_):
            self.asked.append(code)
            raise LookupError("r18.dev 没有这个番号")

        def community(self, code, **_):
            runtime = self.runtimes.get(code)
            if runtime is None:
                raise LookupError("社区来源都没有这个番号")
            # 真实返回顺序里 javbus 排在 javdb 前面,`n0780` 它报的是 36 分。
            return [("javbus", {"runtime": 36}), ("javdb", {"runtime": runtime})]

    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.addCleanup(self.tmp.cleanup)
        self.db = Path(self.tmp.name) / "ledger.db"
        self.connection = sqlite3.connect(self.db)
        self.addCleanup(self.connection.close)
        self.connection.executescript(SCHEMA)

    def _creator(self, entity_id, name, assets):
        self.connection.execute(
            "INSERT INTO entity(id,kind,canonical_name,normalized_name) VALUES(?,'creator',?,?)",
            (entity_id, name, name.casefold()))
        for asset_id, asset_name, path, medium, duration in assets:
            self.connection.execute(
                "INSERT INTO asset(id,location,path,name,medium,creator,duration) "
                "VALUES(?,'local',?,?,?,?,?)",
                (asset_id, path, asset_name, medium, name, duration))
            self.connection.execute(
                "INSERT INTO asset_entity(asset_id,entity_id,role,source) "
                "VALUES(?,?,'creator','legacy:asset')", (asset_id, entity_id))

    def test_a_release_folder_is_confirmed_by_its_runtime_and_an_account_is_not(self):
        self._creator(1, "Tokyo-Hot n0780-HD", [
            (10, "Tokyo-Hot.mp4", r"B:\云下载\Tokyo-Hot n0780-HD\Tokyo-Hot.mp4", "video", 5896.8),
            (11, "封殺001.jpg", r"B:\云下载\Tokyo-Hot n0780-HD\論壇文宣\封殺001.jpg", "image", None)])
        self._creator(2, "banbi_555", [
            (20, "18歳.mp4", r"A:\Pack From Shared\pen\banbi_555\18歳.mp4", "video", 1800.0)])
        self.connection.commit()

        provider = self._Provider({"n0780": 98})
        rows = confirm.examine(self.connection, provider)
        verdicts = {row["creator"]: row["confirmed"] for row in rows}

        self.assertEqual(verdicts, {"Tokyo-Hot n0780-HD": "是", "banbi_555": "否"})
        self.assertEqual(provider.asked, ["n0780", "BANBI-555"])
        # 片长取 javdb 的:同一个番号 javbus 报 36 分,盘里那条 98 分。
        self.assertEqual([row["source"] for row in rows if row["confirmed"] == "是"], ["javdb"])

        counts = audit.apply_rows(
            self.connection, [row for row in rows if row["verdict"] == audit.VERDICT_CODE])
        self.assertEqual(counts["codes"], 1)
        self.assertEqual(
            self.connection.execute("SELECT creator,code FROM asset WHERE id=10").fetchone(),
            (None, "n0780"))
        # 目录里的论坛文宣不是这部片的文件:假创作者照样摘掉,番号不给它。
        self.assertEqual(
            self.connection.execute("SELECT creator,code FROM asset WHERE id=11").fetchone(),
            (None, None))
        # 片长对不上的目录一条都不动。
        self.assertEqual(
            self.connection.execute("SELECT creator FROM asset WHERE id=20").fetchone()[0],
            "banbi_555")

    def test_apply_refuses_to_run_without_a_backup(self):
        parser = confirm.build_parser()
        with self.assertRaises(SystemExit):
            confirm.run(parser.parse_args(["--db", str(self.db), "--apply"]))


if __name__ == "__main__":
    unittest.main()