Ctrl K
ComunidadGuías…

tests/test_metadata.py

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

Archivo original ↗Preguntar a KiBot
import unittest

from peach.metadata import (
    identifies_code,
    CATALOG_EVIDENCE_FIELDS,
    CREDENTIAL_ADVICE,
    auth_error,
    auth_wall_reason,
    collapse_repeated_phrase,
    extract_catalog_evidence,
    extract_peach_fields,
    validate_provider_code,
)


class MetadataProviderTests(unittest.TestCase):
    def test_damaged_source_fields_leave_healthy_fields_available(self):
        fields = extract_peach_fields({'title':'壊\ufffdれた題名', 'maker':'正しいメーカー',
            'release_date':'2026-01-02', 'actresses':[{'japanese_name':'名前\ufffd'}],
            'genres':['未\ufffd分類']})
        self.assertNotIn('title', fields)
        self.assertNotIn('performers', fields)
        self.assertNotIn('tags', fields)
        self.assertEqual(fields['studio']['value'], '正しいメーカー')
        self.assertEqual(fields['release_date']['value'], '2026-01-02')
        self.assertNotIn('title', extract_catalog_evidence({'title':'壊\ufffdれた題名'}))

    def test_valid_non_latin_source_text_and_performer_aliases_remain_available(self):
        fields = extract_peach_fields({'title':'日本語 中文 café',
            'actresses':[{'japanese_name':'正常な名前','aliases':['別名\ufffd']}],
            'maker':'正常な会社'})
        self.assertEqual(fields['title']['value'], '日本語 中文 café')
        self.assertNotIn('performers', fields)
        self.assertEqual(fields['studio']['value'], '正常な会社')

    def test_all_unmapped_genres_are_reviewable_without_fabricated_tags(self):
        fields = extract_peach_fields({"genres": ["未知类别甲", "未知类别乙"]})
        self.assertEqual(fields["tags"]["value"], [])
        self.assertEqual(fields["tags"]["unmapped_genres"], ["未知类别甲", "未知类别乙"])
        self.assertTrue(fields["tags"]["warnings"])

    def test_the_gate_rejects_paths_and_urls(self):
        for unsafe in ("/media/IPX-535.mp4", r"R:\media\IPX-535.mp4", "https://x/IPX-535"):
            with self.subTest(unsafe=unsafe), self.assertRaises(ValueError):
                validate_provider_code(unsafe)

    def test_the_gate_passes_both_separators_of_a_dated_code(self):
        # `_` 是一本道等片商的标识(`catalog_rules._CODE_DATE`)。这道闸拦的是路径、
        # URL 和任意文本;连分隔符一起拦,这些片就一部都查不了。
        self.assertEqual(validate_provider_code("092415_001"), "092415_001")
        self.assertEqual(validate_provider_code("092415-001"), "092415-001")

    def test_the_gate_keeps_tokyo_hot_lowercase(self):
        # Tokyo-Hot 的规范写法是小写(`catalog_rules._TOKYO_HOT_BODY`);跟着别的番号
        # 转大写,拿去查的就是来源不认的写法,这些片一部都查不了。
        self.assertEqual(validate_provider_code("N0646"), "n0646")
        self.assertEqual(validate_provider_code("n646"), "n0646")
        self.assertEqual(validate_provider_code("RED123"), "red-123")

    def test_an_authentication_wall_is_its_own_error_kind(self):
        """401、403 与「跳到登录页」都判成 auth,别的失败不许蹭这一档。"""
        self.assertIn("401", auth_wall_reason(status_code=401))
        self.assertIn("403", auth_wall_reason(status_code=403))
        self.assertIn("/login", auth_wall_reason(
            status_code=200, final_url="https://javdb.com/login?next=/v/abc"))
        self.assertIn("/age_check", auth_wall_reason(
            status_code=200, final_url="https://www.dmm.co.jp/age_check/=/declared=yes/"))
        self.assertIn("javdb 登录页", auth_wall_reason(
            status_code=200, body="<title> 登入 | JavDB</title>".encode()))
        for benign in (dict(status_code=404),
                       dict(status_code=200, final_url="https://javdb.com/v/loginbait"),
                       dict(status_code=503),
                       dict(status_code=200, body=b"<title>ABW-220</title>")):
            with self.subTest(benign=benign):
                self.assertEqual(auth_wall_reason(**benign), "")

    def test_a_bare_403_does_not_tell_the_user_to_swap_credentials(self):
        """403 的成因不止一种,措辞就不许替用户断成因。

        `docs/SOURCING.md` 记着 javbus 与 minnano-av 的实测:那些 403 是出口 IP 被封,
        javdb 那次封了 3~7 日,换 Cookie 一点用没有。「本批停止该来源」这个动作对两种
        403 都对,「换一份凭据」这句话只对其中一种,说出口就是把人引去做错事。
        判据明确的那三种没有这个歧义,仍然直说。
        """
        bare = auth_wall_reason(status_code=403)
        self.assertIn("出口 IP", bare)
        self.assertNotIn(CREDENTIAL_ADVICE, bare)
        for confident in (dict(status_code=401),
                          dict(status_code=200, final_url="https://javdb.com/login"),
                          dict(status_code=200, body="<title>登入 | JavDB</title>".encode()),
                          # 403 也带着登录页的地址,成因就不含糊了。
                          dict(status_code=403, final_url="https://javdb.com/login")):
            with self.subTest(confident=confident):
                reason = auth_wall_reason(**confident)
                self.assertIn(CREDENTIAL_ADVICE, reason)
                self.assertNotIn("出口 IP", reason)

    def test_an_auth_error_is_not_retryable_and_not_a_verdict(self):
        """同一份凭据再问一次还是这个结果,所以不重试;换一份就能继续,所以不是定论。"""
        error = auth_error("mgstage", "来源返回 403", status_code=403)
        self.assertEqual((error.kind, error.status_code), ("auth", 403))
        self.assertFalse(error.retryable)
        self.assertTrue(error.temporary)
        self.assertIn("mgstage", str(error))

    def test_repeated_performer_is_collapsed_before_candidate_creation(self):
        self.assertEqual(collapse_repeated_phrase("木村さん 木村さん"), ("木村さん", True))
        fields = extract_peach_fields({
            "actresses": [
                {"dmm_id": 7, "japanese_name": "木村さん 木村さん"},
                {"dmm_id": 7, "japanese_name": "木村さん"},
                {"dmm_id": 8, "japanese_name": "画像を拡大する 画像を拡大する"},
            ],
            "maker": "Studio Studio", "series": "Series A",
            "release_date": "2020-09-13T00:00:00Z", "genres": ["Anal"],
        })
        self.assertEqual(fields["performers"]["value"], [
            {"name": "木村さん", "external_id": "7", "thumb_url": ""},
        ])
        self.assertEqual(fields["studio"]["value"], "Studio")
        self.assertEqual(fields["release_date"]["value"], "2020-09-13")
        self.assertIn("已规范化", fields["release_date"]["warnings"][0])
        self.assertEqual(fields["tags"]["value"], ["屁眼"])

    def test_performer_profile_names_are_aliases_not_separate_people(self):
        fields = extract_peach_fields({"actresses": [{
            "dmm_id": 1051912, "japanese_name": "涼森れむ",
            "name_kana": "すずもりれむ", "name_romaji": "Remu Suzumori",
            "thumb_url": "https://pics.dmm.co.jp/mono/actjpgs/suzumori_remu.jpg",
            "profile_source": "r18dev",
        }]})
        self.assertEqual(fields["performers"]["value"], [{
            "name": "涼森れむ", "external_id": "1051912",
            "thumb_url": "https://pics.dmm.co.jp/mono/actjpgs/suzumori_remu.jpg",
            "aliases": ["すずもりれむ", "Remu Suzumori"],
            "profile_source": "r18dev",
        }])

    def test_a_bare_name_list_is_read_as_performers(self):
        """只给名字的快照(fc2cmadb 2026-09-22 存下的 `["梨奈"]`)也要进演员候选,不能静默丢掉。"""
        fields = extract_peach_fields({"actresses": ["梨奈", {"japanese_name": "梨奈"}]})
        self.assertEqual(fields["performers"]["value"], [{"name": "梨奈", "external_id": "", "thumb_url": ""}])

    def test_series_and_studio_take_the_japanese_original(self):
        payload = {
            "maker": "Prestige",
            "series": "Prestige 20th Anniversary Special Event",
            "translations": [
                {"language": "en", "maker": "Prestige", "series": "Prestige 20th Anniversary Special Event"},
                {"language": "ja", "maker": "プレステージ", "series": "【プレステージ20周年特別企画】"},
            ],
        }
        fields = extract_peach_fields(payload)
        self.assertEqual(fields["series"]["value"], "【プレステージ20周年特別企画】")
        self.assertEqual(fields["studio"]["value"], "プレステージ")

    def test_catalog_titles_become_reviewable_truth_candidates(self):
        fields = extract_peach_fields({
            "title": "日本語タイトル", "original_title": "Original Title",
        })
        self.assertEqual(fields["title"]["value"], "日本語タイトル")
        self.assertEqual(fields["original_title"]["value"], "Original Title")

    def test_an_empty_japanese_side_keeps_the_brand_but_not_the_translated_series(self):
        """厂牌用品牌名,退回顶层;系列顶层是 r18 的英文译文,退回去就成了英文的系列实体。"""
        fields = extract_peach_fields({
            "maker": "FALENO",
            "series": "FALENO Compilation",
            "translations": [{"language": "ja", "maker": "", "series": ""}],
        })
        self.assertNotIn("series", fields)
        self.assertEqual(fields["studio"]["value"], "FALENO")

    def test_rich_catalog_fields_stay_source_evidence(self):
        evidence = extract_catalog_evidence({
            "title": "English title", "original_title": "原标题", "runtime": "121",
            "director": "Director A", "label": "Label A",
            "poster_url": "https://img.example/poster.jpg",
            "cover_url": "https://img.example/cover.jpg",
            "screenshot_urls": [
                "https://img.example/1.jpg", "https://img.example/1.jpg",
                "file:///private/2.jpg", "https://img.example/2.jpg",
            ],
            "trailer_url": "https://video.example/trailer.m3u8",
            "translations": [{
                "language": "ja", "title": "日本語タイトル", "label": "日本レーベル",
            }],
        })
        self.assertEqual(set(evidence), set(CATALOG_EVIDENCE_FIELDS))
        self.assertEqual(evidence["title"]["value"], "日本語タイトル")
        self.assertEqual(evidence["original_title"]["value"], "原标题")
        self.assertEqual(evidence["runtime"]["value"], 121)
        self.assertEqual(evidence["label"]["value"], "日本レーベル")
        self.assertEqual(evidence["screenshot_urls"]["value"], [
            "https://img.example/1.jpg", "https://img.example/2.jpg",
        ])
        self.assertIn("2 张截图", evidence["screenshot_urls"]["display_value"])

    def test_catalog_evidence_rejects_credentialed_and_non_http_urls(self):
        evidence = extract_catalog_evidence({
            "title": "Same", "original_title": "Same", "runtime": 0,
            "poster_url": "https://user:secret@example.test/poster.jpg",
            "cover_url": "R:/covers/ABC-001.jpg",
            "trailer_url": "javascript:alert(1)",
        })
        self.assertEqual(evidence, {"title": {
            "value": "Same", "display_value": "Same", "warnings": [],
        }})

class SourceIdentityTests(unittest.TestCase):
    def test_explicit_release_suffix_is_not_satisfied_by_a_plain_or_different_product(self):
        self.assertTrue(identifies_code("DANDY-851C", {"content_id": "1dandy00851c"}))
        self.assertFalse(identifies_code("DANDY-851C", {"content_id": "1dandy00851"}))
        self.assertFalse(identifies_code("DANDY-851C", {"content_id": "1dandy00851ch"}))
        self.assertFalse(identifies_code("DANDY-851", {"content_id": "1dandy00851c"}))

    def test_prefixed_release_requires_matching_product_evidence(self):
        dvd = {"id": "JAC-040", "content_id": "118jac040",
               "source_url": "https://www.dmm.co.jp/mono/dvd/-/detail/=/cid=118jac040/"}
        self.assertFalse(identifies_code("390JAC-040", dvd))
        self.assertTrue(identifies_code("JAC-040", dvd))
        self.assertTrue(identifies_code("390JAC-040", {
            "id": "JAC-040", "source_url": "https://www.mgstage.com/product/product_detail/390JAC-040/"}))

    def test_verified_mgstage_prefixes_match_the_bare_dmm_release(self):
        """2026-09-25 逐组核过的 MGStage 前缀与 DMM 裸编号是同一作品,同轮的反例仍拒。"""
        for code, payload in (
                ("476MLA-234", {"id": "MLA-234", "content_id": "mla234"}),
                ("428SUKE-080", {"id": "SUKE-080", "content_id": "h_1711suke00080"}),
                ("336KBI-010", {"id": "KBI-010", "content_id": "118kbi00010"}),
                ("762FKOS-001", {"id": "FKOS-001", "content_id": "h_1721fkos00001"})):
            self.assertTrue(identifies_code(code, payload), code)
        for code, payload in (
                ("348NTR-007", {"id": "NTR-007", "content_id": "1ntr00007"}),
                ("451HHH-022", {"id": "HHH-022", "content_id": "62hhh00022"}),
                ("550ENE-006", {"id": "ENE-006", "content_id": "ene006"})):
            self.assertFalse(identifies_code(code, payload), code)

    def test_a_registered_short_serial_rejects_the_padded_release(self):
        # #296:`MCDV-24` 与 `MCDV-024` 是两部片,DMM 按补零猜出的 `mcdv00024` 是后者。
        self.assertFalse(identifies_code("MCDV-24", {"content_id": "mcdv00024"}))
        self.assertTrue(identifies_code("MCDV-024", {"content_id": "mcdv00024"}))
        self.assertTrue(identifies_code("MCDV-24", {"id": "MCDV-24"}))

    def test_real_source_id_shapes_are_accepted(self):
        # DMM 带厂牌数字前缀,r18dev 对 IQQQ-026 会补零,259 系只在 URL 里出现番号。
        self.assertTrue(identifies_code("ABW-220", {"content_id": "118abw220"}))
        self.assertTrue(identifies_code("IQQQ-026", {"content_id": "h_086iqqq00026"}))
        self.assertTrue(identifies_code("MESU-088", {"id": "MESU-88"}))
        self.assertTrue(identifies_code("259LUXU-1475", {
            "source_url": "https://www.mgstage.com/product/product_detail/259LUXU-1475/",
        }))

    def test_unrelated_product_is_rejected_as_not_found(self):
        # dl.getchu 对 ABW-220 与 259LUXU-1475 都返回同一件同人商品 item33938。
        payload = {"id": "33938", "content_id": "33938",
                   "source_url": "https://dl.getchu.com/i/item33938"}
        self.assertFalse(identifies_code("ABW-220", payload))
        self.assertFalse(identifies_code("259LUXU-1475", payload))
        self.assertFalse(identifies_code("ABW-220", {}))
        # 番号相邻不等于同一部:ABW-2200 不能拿 ABW-220 的结果顶替。
        self.assertFalse(identifies_code("ABW-2200", {"content_id": "118abw220"}))
        self.assertFalse(identifies_code("IY-104", {
            "id": "DIY-104", "source_url": "https://www.javbus.com/ja/DIY-104"}))
        self.assertFalse(identifies_code("DB-202", {"id": "DDB-202"}))
        self.assertTrue(identifies_code("JBS-023", {
            "source_url": "https://www.javbus.com/ja/JBS-023"}))

    def test_fc2_bare_digits_are_the_same_release(self):
        """FC2 来源只给裸数字,那仍然是同一部作品。

        判据交给 `same_release_code`,它认得这种写法;照形状比对的那一套不认,
        于是 `FC2-PPV-3701252` 和来源返回的 `3701252` 被判成两部不同的作品。
        """
        for payload in (
            {"id": "3701252", "content_id": "3701252",
             "source_url": "https://fc2cmadb.com/articles/3701252"},
            {"content_id": "3701252"},
        ):
            self.assertTrue(identifies_code("FC2-PPV-3701252", payload))
        self.assertFalse(identifies_code("FC2-PPV-3701252", {"id": "3701253"}))

    def test_javbus_loose_search_results_are_a_different_release(self):
        """javbus 搜不到就返回首个近似命中,两个方向都要认出来。

        字母段被补长(`AR-101` 取回 `STAR-101`)和数字段被补长(`259LUXU-764`
        取回 `259LUXU-1764`)都是别的作品,来源自报的番号是唯一能认出它的证据。
        """
        for code, returned in (
            ("AR-101", "STAR-101"), ("AR-102", "ZMAR-102"), ("WX17", "WXSD-017"),
            ("CD-101", "BCDP-101"), ("259LUXU-764", "259LUXU-1764"),
            ("259LUXU-164", "259LUXU-1642"),
        ):
            self.assertFalse(identifies_code(code, {
                "id": returned, "content_id": returned,
                "source_url": f"https://www.javbus.com/ja/{returned}"}), f"{code} <- {returned}")



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