tests/test_metadata_creator_attributions.py
Rama master · réplica de lectura del repositorio original.
"""创作者审计保留独立身份、保护人工判断,并可整批回滚。"""
import sqlite3
import tempfile
import unittest
import json
from pathlib import Path
from types import SimpleNamespace
from peach.classification import creator_collection_base, is_structural_creator
from peach.entities import (apply_directory_rejections, creator_directory_key, derived_directory_rejections,
merge_entity, normalize_entity_name, upsert_asset_entity)
from peach.field_owners import write_owned_fields
from peach.entity_classification import write_claim
from peach.metadata_creator_attributions import apply_plan, collect, restore
from tests.support.ledger import fresh_ledger
from scripts import audit_creator_attributions as audit
class CreatorAttributionTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.db = fresh_ledger(Path(self.directory.name).resolve())
self.connection = sqlite3.connect(self.db)
self.addCleanup(self.connection.close)
def asset(self, name, *, path=None, studio=None, code=None, source='legacy:asset', metadata=None):
cursor = self.connection.execute(
"INSERT INTO asset(location,path,name,medium,creator,studio,code) VALUES('local',?,?,'video',?,?,?)",
(path or f'B:\\创作者\\{name}\\clip.mp4', (path or 'clip.mp4').rsplit('\\', 1)[-1], name, studio, code))
asset_id = cursor.lastrowid
# 历史错误直接造在夹具里,摄取入口本身须拒绝发行平台。
held = self.connection.execute("SELECT id FROM entity WHERE kind='creator' AND canonical_name=?", (name,)).fetchone()
if held:
entity_id = held[0]
else:
entity_id = self.connection.execute(
"INSERT INTO entity(kind,canonical_name,normalized_name,created_at,updated_at) VALUES('creator',?,?,'t','t')",
(name, name.casefold())).lastrowid
self.connection.execute("INSERT INTO asset_entity(asset_id,entity_id,role,source,metadata_json) VALUES(?,?,'creator',?,?)",
(asset_id, entity_id, source, metadata or '{}'))
return asset_id, entity_id
def plan(self):
return [row for row in collect(self.connection) if row['action'] in {'remove','replace'}]
def test_rejected_account_attribution_blocks_directory_reingest_and_allows_manual_review(self):
asset_id, entity_id = self.asset('Parent Account')
self.connection.execute("DELETE FROM asset_entity WHERE asset_id=?", (asset_id,))
self.connection.execute("INSERT INTO review_decision(category,item_key,status,updated_at) "
"VALUES('creator-attribution',?,'rejected','t')", (f'{asset_id}:parent account',))
self.assertIsNone(upsert_asset_entity(self.connection, kind='creator',name='Parent Account',
asset_id=asset_id,role='creator',source='scan:directory'))
self.assertEqual(self.connection.execute('SELECT count(*) FROM asset_entity WHERE asset_id=?',(asset_id,)).fetchone()[0],0)
self.assertEqual(upsert_asset_entity(self.connection,kind='creator',name='Parent Account',
asset_id=asset_id,role='creator',source='user:manual'),entity_id)
def test_collection_quality_suffix_uses_the_rejected_canonical_account_decision(self):
asset_id, _ = self.asset('Parent Account')
self.connection.execute("INSERT INTO review_decision(category,item_key,status,updated_at) "
"VALUES('creator-attribution',?,'rejected','t')", (f'{asset_id}:parent account',))
self.assertIsNone(upsert_asset_entity(self.connection, kind='creator',name='Parent Account4K',
asset_id=asset_id,role='creator',source='scan:directory'))
def test_directory_rejection_covers_new_files_and_subdirectories(self):
self.connection.execute("INSERT INTO review_decision(category,item_key,status,updated_at) "
"VALUES('creator-attribution',?,'rejected','t')",
(creator_directory_key('Parent Account', r'B:\创作者\Parent Account\Other Channel'),))
blocked = [r'B:\创作者\Parent Account\Other Channel\new.mp4',
r'B:\创作者\Parent Account\other channel\图片\new.jpg']
for path in blocked:
asset_id, _ = self.asset('Parent Account', path=path)
with self.subTest(path=path):
self.assertIsNone(upsert_asset_entity(self.connection, kind='creator', name='Parent Account',
asset_id=asset_id, role='creator', source='scan:directory'))
self.assertIsNotNone(upsert_asset_entity(self.connection, kind='creator', name='Parent Account',
asset_id=asset_id, role='creator', source='user:manual'))
own_id, entity_id = self.asset('Parent Account', path=r'B:\创作者\Parent Account\own.mp4')
self.assertEqual(upsert_asset_entity(self.connection, kind='creator', name='Parent Account',
asset_id=own_id, role='creator', source='scan:directory'), entity_id)
def test_fully_rejected_subtrees_yield_topmost_directory_rejections(self):
root = r'B:\创作者\Parent Account'
rejected = [rf'{root}\Other Channel\a.mp4', rf'{root}\Other Channel\图片\b.jpg',
rf'{root}\Mixed\c.mp4', r'B:\卖家\Reseller\Parent Account\e.mp4']
for path in rejected:
asset_id, _ = self.asset('Parent Account', path=path)
self.connection.execute("INSERT INTO review_decision(category,item_key,status,note,updated_at) "
"VALUES('creator-attribution',?,'rejected','另一账号','t')", (f'{asset_id}:parent account',))
self.asset('Parent Account', path=rf'{root}\Mixed\own.mp4')
proposals = derived_directory_rejections(self.connection)
self.assertEqual([(row['directory'], row['subtree_assets']) for row in proposals],
[(rf'{root}\Other Channel', 2), (r'B:\卖家\Reseller\Parent Account', 1)])
self.assertEqual(apply_directory_rejections(self.connection, proposals, reviewer='script:test'), 2)
self.assertEqual(derived_directory_rejections(self.connection), [])
new_id, _ = self.asset('Elsewhere', path=rf'{root}\Other Channel\新\d.mp4')
self.assertIsNone(upsert_asset_entity(self.connection, kind='creator', name='Parent Account',
asset_id=new_id, role='creator', source='scan:directory'))
def test_collection_suffix_respects_rejection_without_a_canonical_account(self):
asset_id, _ = self.asset('Parent Account4K')
self.connection.execute("INSERT INTO review_decision(category,item_key,status,updated_at) "
"VALUES('creator-attribution',?,'rejected','t')", (f'{asset_id}:parent account',))
self.assertIsNone(upsert_asset_entity(self.connection, kind='creator', name='Parent Account4K',
asset_id=asset_id, role='creator', source='scan:directory'))
def test_merged_person_alias_ingests_into_the_preserved_artist(self):
asset_id, creator_id = self.asset('Former Name')
artist_id = upsert_asset_entity(self.connection,kind='performer',name='Current Name',
asset_id=asset_id,role='performer',source='release:cast',metadata={'official':'preserved'})
merge_entity(self.connection,target_id=artist_id,source_id=creator_id,source_name='Former Name',
alias_source='user:identity-merge:creator')
another_id, _ = self.asset('Independent Account')
found = upsert_asset_entity(self.connection,kind='creator',name='Former Name',
asset_id=another_id,role='creator',source='scan:directory',metadata={'other':'data'})
self.assertEqual(found,artist_id)
self.assertIsNone(self.connection.execute("SELECT id FROM entity WHERE kind='creator' AND canonical_name='Former Name'").fetchone())
self.assertEqual(self.connection.execute('SELECT role FROM asset_entity WHERE asset_id=? AND entity_id=?',
(another_id,artist_id)).fetchone(),('performer',))
self.assertEqual(json.loads(self.connection.execute('SELECT metadata_json FROM entity WHERE id=?',
(artist_id,)).fetchone()[0]),{'official':'preserved'})
def test_unmerged_artist_alias_keeps_an_independent_account(self):
asset_id, _ = self.asset('Independent Account')
artist_id = upsert_asset_entity(self.connection,kind='performer',name='Current Name',
asset_id=asset_id,role='performer',source='release:cast')
self.connection.execute("INSERT INTO entity_alias VALUES(?, 'Shared Alias','shared alias','source:directory',1)",(artist_id,))
found = upsert_asset_entity(self.connection,kind='creator',name='Shared Alias',
asset_id=asset_id,role='creator',source='scan:directory')
self.assertNotEqual(found,artist_id)
def test_content_month_quality_and_repost_directories_are_not_accounts(self):
names = ['zj','11月','AI修复','黑丝','美腿足交','7sht.me','98T.la202202092146']
for name in names:
asset_id, _ = self.asset(name)
self.assertIsNone(upsert_asset_entity(self.connection,kind='creator',name=name,
asset_id=asset_id,role='creator',source='legacy:asset'))
self.asset('Santa'); self.asset('banbi_555'); self.asset('alice.example.com')
self.assertCountEqual([row['current_creator'] for row in self.plan()],names)
self.asset('zj',path=r'B:\手动归属\zj\clip.mp4',source='user:manual')
self.assertCountEqual([row['current_creator'] for row in self.plan()],names[1:])
self.assertEqual({row['relation_source']:row['action'] for row in collect(self.connection)
if row['current_creator']=='zj'}, {'legacy:asset':'review', 'user:manual':'keep'})
def test_directory_names_built_from_genre_vocabulary_are_not_accounts(self):
for name in ('黑丝美腿', 'jk足交合集', '极品美腿天花板', '合集-巨乳 多创作者', '美腿 1080p',
'2023年5月', '4K', '맨발_모음', '검스A맨발B_풋잡', '&网红套图(走光)'):
with self.subTest(name=name):
self.assertTrue(is_structural_creator(name))
for name in ('Alice', '海生', 'jk小美', 'fc2美臀OLさくら', 'Lena Anderson', 'analove', '黑丝小仙女'):
with self.subTest(name=name):
self.assertFalse(is_structural_creator(name))
def test_collection_annotations_reduce_to_the_account_name(self):
for name in ('someone 合集', 'someone合辑', 'someone全集', 'someone最新12v', 'someone 30v 40g',
'someone V12 80GB', 'someone(2)', 'someone 去重版 4K', 'someone 120GB', 'someone 1080p'):
with self.subTest(name=name):
self.assertEqual(creator_collection_base(name), 'someone')
for name in ('someone 2509', 'someone', 'v12', '合集'):
with self.subTest(name=name):
self.assertEqual(creator_collection_base(name), name)
def test_month_and_quality_collections_reuse_accounts_and_restore_all_relations(self):
_, target = self.asset('muchi_tina',source='user:manual')
ids = [self.asset(name)[0] for name in ['muchi_tina2025.4','muchi_tina10月','muchi_tina2025年一月']]
self.connection.commit()
frozen = self.plan()
self.assertEqual([row['proposed_creator'] for row in frozen],['muchi_tina']*3)
with self.connection:
receipt = apply_plan(self.connection,frozen)
for asset_id in ids:
self.assertEqual(self.connection.execute('SELECT creator FROM asset WHERE id=?',(asset_id,)).fetchone()[0],'muchi_tina')
self.assertEqual(self.connection.execute("SELECT entity_id FROM asset_entity WHERE asset_id=? AND role='creator'",(asset_id,)).fetchone()[0],target)
with self.connection:
restore(self.connection,receipt)
self.assertEqual(len(self.plan()),3)
self.assertEqual(self.connection.execute('PRAGMA foreign_key_check').fetchall(),[])
def test_versioned_release_directories_and_publisher_numbers_are_excluded(self):
samples = {
'WAAA-415_6K-C':r'B:\云下载\WAAA-415_6K-C\@Milan@ty999.me_WAAA-415_6K-C.mp4',
'wavr00178pl':r'B:\云下载\wavr00178pl\wavr00178.part1.mp4',
'fellatiojapan-090-':r'B:\创作者\fellatiojapan-090-\fellatiojapan-090-AyumuNakahori-1080p\090-AyumuNakahori-1080p.mp4',
'fellatiojapan-324':r'B:\云下载\fellatiojapan-324\2048社区 - big2048.com@fellatiojapan-324.mp4',
}
for name,path in samples.items():
asset_id,_ = self.asset(name,path=path)
self.assertIsNone(upsert_asset_entity(self.connection,kind='creator',name=name,
asset_id=asset_id,role='creator',source='scan:directory'))
self.asset('banbi_555',path=r'B:\创作者\banbi_555\scene title.mp4')
self.asset('ABW-987',path=r'B:\创作者\ABW-987\another title.mp4')
self.assertCountEqual([row['current_creator'] for row in self.plan()],samples)
def test_release_directory_does_not_assign_its_code_to_unrelated_files(self):
examples = [('WAAA-415_6K-C', 'HMN-597.mp4'),
('WAAA-415_6K-C', 'daily life.mp4'),
('gachincoppv-1009-HD', 'gachincoppv-1009-HD1.wmv'),
('gachincoppv-1009-HD', 'gachincoppv-1009-HD3.wmv')]
ids = []
for name, filename in examples:
asset_id, _ = self.asset(name, path=f'B:\\云下载\\{name}\\{filename}')
ids.append(asset_id)
self.assertIsNone(upsert_asset_entity(self.connection,kind='creator',name=name,
asset_id=asset_id,role='creator',source='scan:directory'))
self.asset('banbi_555', path=r'B:\创作者\banbi_555\holiday.mp4')
frozen = self.plan()
self.assertCountEqual([row['asset_id'] for row in frozen], ids)
with self.connection:
receipt = apply_plan(self.connection, frozen)
self.assertEqual(self.connection.execute('SELECT count(*) FROM asset WHERE creator IS NULL AND code IS NULL').fetchone()[0], 4)
self.assertEqual(self.connection.execute('SELECT count(*) FROM asset').fetchone()[0], 5)
with self.connection:
restore(self.connection, receipt)
self.assertCountEqual([row['asset_id'] for row in self.plan()], ids)
def test_tokyo_publisher_requires_registered_studio_and_matching_release_file(self):
asset_id,_ = self.asset('Tokyo',studio='东京热',code='n1042',path=r'B:\云下载\TokyoHot-n1042.mp4')
studio_id = upsert_asset_entity(self.connection,kind='studio',name='东京热',
asset_id=asset_id,role='studio',source='release:studio')
self.connection.execute("INSERT INTO entity_alias(entity_id,alias,normalized_alias,source) VALUES(?,'Tokyo Hot','tokyo hot','release')",(studio_id,))
self.asset('Tokyo',studio='东京热',code='n1042',path=r'B:\创作者\Tokyo\unrelated.mp4')
self.assertEqual([row['asset_id'] for row in self.plan()],[asset_id])
def test_source_confirmed_publisher_lands_as_studio_and_is_reversible(self):
asset_id, entity_id = self.asset('Published Brand')
write_claim(self.connection,entity_id=entity_id,facet='account_role',value='studio',
source='source:official',source_url='https://publisher.test/',evidence='发行目录将出演者与厂牌分列',
status='observed',confidence=1)
self.connection.commit()
self.assertIsNone(upsert_asset_entity(self.connection,kind='creator',name='Published Brand',
asset_id=asset_id,role='creator',source='scan:directory'))
self.assertIsNone(upsert_asset_entity(self.connection,kind='creator',name='Published Brand2025.05',
asset_id=asset_id,role='creator',source='scan:directory'))
with self.connection:
receipt = apply_plan(self.connection,self.plan())
self.assertEqual(self.connection.execute('SELECT creator,studio FROM asset WHERE id=?',(asset_id,)).fetchone(),(None,'Published Brand'))
self.assertEqual(len(receipt['created_entities']),1)
with self.connection:
restore(self.connection,receipt)
self.assertEqual(self.connection.execute('SELECT creator,studio FROM asset WHERE id=?',(asset_id,)).fetchone(),('Published Brand',None))
self.assertEqual(self.connection.execute('PRAGMA foreign_key_check').fetchall(),[])
def test_confirmed_studio_and_aliases_are_excluded_from_automatic_creator_ingest(self):
asset_id, entity_id = self.asset('Published Brand')
write_claim(self.connection,entity_id=entity_id,facet='account_role',value='studio',
source='source:official',source_url='https://publisher.test/',evidence='发行厂牌官网',
status='observed',confidence=1)
self.connection.execute("INSERT INTO entity_alias(entity_id,alias,normalized_alias,source) VALUES(?,'Brand.test','brand.test','release')",(entity_id,))
receipt = apply_plan(self.connection,self.plan())
studio_id = receipt['created_entities'][0]['id']
merge_entity(self.connection,target_id=studio_id,source_id=entity_id,
source_name='Published Brand',alias_source='user:identity-merge:creator')
for name in ('Published Brand','Published Brand2025.05','Brand.test','Brand.test2025.05'):
with self.subTest(name=name):
self.assertIsNone(upsert_asset_entity(self.connection,kind='creator',name=name,
asset_id=asset_id,role='creator',source='scan:directory'))
self.assertEqual(self.connection.execute("SELECT count(*) FROM entity WHERE kind='creator'").fetchone()[0],0)
self.assertEqual(self.connection.execute('SELECT entity_id,role FROM asset_entity WHERE asset_id=?',(asset_id,)).fetchall(),[(studio_id,'studio')])
self.assertEqual(self.connection.execute('PRAGMA foreign_key_check').fetchall(),[])
manual_id = upsert_asset_entity(self.connection,kind='creator',name='Published Brand',
asset_id=asset_id,role='creator',source='user:manual')
self.assertIsNotNone(manual_id)
def test_unconfirmed_studio_claim_does_not_block_creator_ingest(self):
asset_id, _ = self.asset('Alice')
studio_id = upsert_asset_entity(self.connection,kind='studio',name='Brand',
asset_id=asset_id,role='studio',source='source:catalog')
write_claim(self.connection,entity_id=studio_id,facet='account_role',value='studio',
source='source:search',evidence='待核验的同名检索',status='candidate',confidence=0.5)
self.assertIsNotNone(upsert_asset_entity(self.connection,kind='creator',name='Brand',
asset_id=asset_id,role='creator',source='scan:directory'))
def test_created_studio_with_subsequent_alias_refuses_atomic_restore(self):
asset_id, entity_id = self.asset('Published Brand')
write_claim(self.connection,entity_id=entity_id,facet='account_role',value='studio',
source='source:official',source_url='https://publisher.test/',evidence='发行厂牌官网',
status='observed',confidence=1)
self.connection.commit()
with self.connection:
receipt = apply_plan(self.connection,self.plan())
studio_id = receipt['created_entities'][0]['id']
self.connection.execute("INSERT INTO entity_alias(entity_id,alias,normalized_alias,source) VALUES(?,'Brand','brand','user:manual')",(studio_id,))
self.connection.commit()
with self.assertRaises(ValueError):
with self.connection:
restore(self.connection,receipt)
self.assertEqual(self.connection.execute('SELECT creator,studio FROM asset WHERE id=?',(asset_id,)).fetchone(),(None,'Published Brand'))
self.assertEqual(self.connection.execute('SELECT count(*) FROM entity_alias WHERE entity_id=?',(studio_id,)).fetchone()[0],1)
def test_collection_ingest_reuses_existing_account_and_preserves_profile(self):
_, target = self.asset('muchi_tina',source='user:manual')
self.connection.execute("UPDATE entity SET metadata_json='{\"profile\":\"kept\"}' WHERE id=?",(target,))
asset_id,_ = self.asset('other')
returned = upsert_asset_entity(self.connection,kind='creator',name='muchi_tina2025.05',
asset_id=asset_id,role='creator',source='scan:directory')
self.assertEqual(returned,target)
self.assertEqual(self.connection.execute('SELECT metadata_json FROM entity WHERE id=?',(target,)).fetchone()[0],'{"profile":"kept"}')
def test_platform_is_not_ingested_as_creator(self):
asset_id, _ = self.asset('RealSeller')
entity_id = upsert_asset_entity(self.connection, kind='creator', name='Myfans',
asset_id=asset_id, role='creator', source='user:manual')
self.assertIsNone(entity_id)
self.assertEqual(self.connection.execute("SELECT count(*) FROM entity WHERE canonical_name='Myfans'").fetchone()[0], 0)
def test_audit_covers_all_sources_and_preserves_accounts_and_manual_identity(self):
self.asset('Myfans')
self.asset('Alice', path='https://example.test/users/alice')
self.asset('ConfirmedSeller', source='user:watermark')
self.asset('banbi_555', path=r'B:\创作者\banbi_555\a title.mp4')
self.asset('FC2-PPV', source='user:manual')
rows = collect(self.connection)
self.assertEqual(len(rows), 5)
self.assertEqual([row['current_creator'] for row in rows if row['action']=='remove'], ['Myfans'])
self.assertEqual({row['verdict'] for row in rows}, {'platform','online_account','source_identity','identifier_candidate'})
def test_studio_aliases_and_dated_scene_titles_have_file_evidence(self):
asset_id, _ = self.asset('Das', studio='ダスッ!')
studio_id = upsert_asset_entity(self.connection, kind='studio', name='ダスッ!',
asset_id=asset_id, role='studio', source='release:studio')
self.connection.execute("INSERT INTO entity_alias(entity_id,alias,normalized_alias,source) VALUES(?,'Das','das','release')", (studio_id,))
self.asset('Freya Mayer Ready to Serve You', path=r'B:\西方\DarkRoomVR.21.04.02.Freya.Mayer.Ready.to.Serve.You.XXX\movie.mp4', studio='DarkRoomVR')
self.asset('Jane Doe', path=r'B:\西方\Studio.21.04.02.Jane.Doe.XXX\movie.mp4')
self.asset('DPMI-058_000', path=r'B:\云下载\DPMI-058_000\dpmi00058hhb_000.mp4')
self.asset('ABW-123', path=r'B:\云下载\ABW-123\ABW-123.mp4', code='ABW-123')
self.assertEqual({row['current_creator']: row['verdict'] for row in self.plan()}, {
'Das':'studio', 'Freya Mayer Ready to Serve You':'release_title',
'DPMI-058_000':'release_identifier', 'ABW-123':'release_identifier'})
def test_repair_and_restore_preserve_identity_metadata_and_personal_records(self):
asset_id, entity_id = self.asset('Myfans', metadata='{"evidence":"folder"}')
self.connection.execute('UPDATE asset SET play_count=9,rating=4 WHERE id=?', (asset_id,))
self.connection.commit()
plan = self.plan()
with self.connection:
manifest = apply_plan(self.connection, plan)
self.assertEqual(len(manifest['relations']), 1)
self.assertEqual(self.connection.execute('SELECT creator,play_count,rating FROM asset WHERE id=?', (asset_id,)).fetchone(), (None,9,4))
self.assertEqual(self.connection.execute('SELECT canonical_name FROM entity WHERE id=?', (entity_id,)).fetchone(), ('Myfans',))
with self.connection:
restore(self.connection, manifest)
self.assertEqual(self.connection.execute('SELECT creator,play_count,rating FROM asset WHERE id=?', (asset_id,)).fetchone(), ('Myfans',9,4))
self.assertEqual(self.connection.execute('SELECT metadata_json FROM asset_entity WHERE asset_id=?', (asset_id,)).fetchone(), ('{"evidence":"folder"}',))
self.assertEqual(self.connection.execute('PRAGMA foreign_key_check').fetchall(), [])
def test_stale_plan_and_later_edits_reject_whole_batch(self):
asset_id, _ = self.asset('Myfans')
other_id, _ = self.asset('FC2-PPV')
self.connection.commit()
plan = self.plan()
write_owned_fields(self.connection, [asset_id], {'creator':'Seller'}, 'user:manual')
self.connection.commit()
with self.assertRaisesRegex(ValueError, '计划'):
with self.connection:
apply_plan(self.connection, plan)
self.assertEqual(self.connection.execute('SELECT count(*) FROM asset_entity').fetchone()[0], 2)
with self.connection:
manifest = apply_plan(self.connection, self.plan())
write_owned_fields(self.connection, [other_id], {'creator':'VerifiedSeller'}, 'user:manual')
self.connection.commit()
with self.assertRaisesRegex(ValueError, '后续改动'):
with self.connection:
restore(self.connection, manifest)
self.assertEqual(self.connection.execute('SELECT creator FROM asset WHERE id=?', (other_id,)).fetchone()[0], 'VerifiedSeller')
def test_protected_and_independent_identity_are_reviewed(self):
asset_id, entity_id = self.asset('Myfans')
write_owned_fields(self.connection, [asset_id], {'creator':'Myfans'}, 'review:identity')
self.asset('FC2-PPV')
self.connection.execute("INSERT INTO entity_external_ref(entity_id,provider,external_kind,external_id) VALUES(?, 'site','creator','seller-12')", (entity_id,))
self.assertEqual([row['current_creator'] for row in self.plan()], ['FC2-PPV'])
def test_publisher_moves_to_existing_studio_and_restores_as_a_batch(self):
studio_asset, _ = self.asset('Alice')
studio_id = upsert_asset_entity(self.connection, kind='studio', name='Vixen',
asset_id=studio_asset, role='studio', source='release:studio', metadata={'official':'preserved'})
studio_before = self.connection.execute('SELECT metadata_json,updated_at FROM entity WHERE id=?', (studio_id,)).fetchone()
asset_id, creator_id = self.asset('Vixen', path=r'B:\云下载\A Scene [Vixen.com]\scene.mp4')
self.connection.commit()
with self.connection:
manifest = apply_plan(self.connection, self.plan())
self.assertEqual(self.connection.execute('SELECT creator,studio FROM asset WHERE id=?', (asset_id,)).fetchone(), (None,'Vixen'))
self.assertEqual(self.connection.execute('SELECT entity_id,role FROM asset_entity WHERE asset_id=?', (asset_id,)).fetchall(), [(studio_id,'studio')])
self.assertEqual(self.connection.execute('SELECT metadata_json,updated_at FROM entity WHERE id=?', (studio_id,)).fetchone(), studio_before)
with self.connection:
restore(self.connection, manifest)
self.assertEqual(self.connection.execute('SELECT creator,studio FROM asset WHERE id=?', (asset_id,)).fetchone(), ('Vixen',None))
self.assertEqual(self.connection.execute('SELECT entity_id,role FROM asset_entity WHERE asset_id=?', (asset_id,)).fetchall(), [(creator_id,'creator')])
def test_site_post_identity_requires_matching_file_and_preserves_accounts(self):
self.asset('[7sht.me]legsjapan-955', path=r'B:\云下载\[7sht.me]legsjapan-955\legsjapan-955.mp4')
self.asset('fellatiojapan-312', path=r'B:\云下载\fellatiojapan-312\fellatiojapan-312.mp4')
self.asset('legsjapan-999', path=r'B:\创作者\legsjapan-999\a title.mp4')
self.assertEqual({row['current_creator'] for row in self.plan()}, {'[7sht.me]legsjapan-955','fellatiojapan-312'})
def test_restore_refuses_a_creator_relation_added_without_flat_field_changes(self):
asset_id, _ = self.asset('Myfans')
self.connection.commit()
with self.connection:
manifest = apply_plan(self.connection, self.plan())
upsert_asset_entity(self.connection, kind='creator', name='NewSeller', asset_id=asset_id,
role='creator', source='user:manual')
self.connection.commit()
with self.assertRaisesRegex(ValueError, '关系已发生后续改动'):
with self.connection:
restore(self.connection, manifest)
def performers(self, *names):
for name in names:
self.connection.execute("INSERT INTO entity(kind,canonical_name,normalized_name,created_at,updated_at) "
"VALUES('performer',?,?,'t','t')", (name, normalize_entity_name(name)))
def shaped(self, account, filenames, source='legacy:asset'):
for filename in filenames:
self.asset(account, path=f'B:\\创作者\\{account}\\{filename}', source=source)
return {row['verdict'] for row in collect(self.connection) if row['current_creator'] == account}
def test_series_naming_with_rotating_cast_is_a_review_candidate_not_a_repair(self):
self.performers('八ッ橋さい子', '本多由奈', '北川ゆず')
self.assertEqual(self.shaped('COSH こすっち', ['こすっち001 八ッ橋さい子.mp4', 'こすっち002 八ッ橋さい子.mp4',
'こすっち003 本多由奈.mp4', 'こすっち005 北川ゆず.mp4']), {'label_shape'})
uralesbian = ['Akari Asagiri & Eri Hirasawa - 120 - Sensual Kiss.mp4',
'Shino Aoi & Mai Miori - 108 - Double Masturbation.mp4', 'ChieKobayashi-ShinoAoi-039-1080p.mp4']
self.assertEqual(self.shaped('UraLesbian', uralesbian, source='user:manual-organize'), {'label_shape'})
self.assertEqual(self.plan(), [])
def test_series_naming_does_not_hold_back_a_verified_studio_repair(self):
_, entity_id = self.asset('Label Works', path='B:\\创作者\\Label Works\\Ann Lee & Mia Rose - 01 - Kiss.mp4')
write_claim(self.connection, entity_id=entity_id, facet='account_role', value='studio', source='source:publisher',
source_url='https://label.test/', evidence='官网', status='observed', confidence=1)
verdicts = self.shaped('Label Works', ['Ann Lee & Leo King - 02 - Pool.mp4', 'Sam Fox & Mia Rose - 03 - Day.mp4'])
self.assertEqual(verdicts, {'verified_studio'})
def test_couple_accounts_titles_and_stray_files_keep_their_account_verdicts(self):
self.performers('Mia Rose', 'Leo King', 'Ann Lee')
couple = ['Mia Rose & Leo King - 01 - Beach.mp4', 'Mia Rose & Ann Lee - 02 - Pool.mp4',
'Mia Rose & Leo King - 03 - Night.mp4', 'Mia Rose & Sam Fox - 04 - Day.mp4']
titles = ['Account 012 Beach Day.mp4', 'Account 013 Hot Tub.mp4', 'Account 014 Night Out.mp4']
stray = ['MariHirose-RiaKurumi-488-1080p.mp4', 'clip1.mp4', 'clip2.mp4', 'clip3.mp4']
for account, names in [('Couple', couple), ('Account', titles), ('RiaKurumi', stray)]:
self.assertNotIn('label_shape', self.shaped(account, names))
def test_cli_preserves_existing_foreign_key_violations_and_keeps_a_restore_manifest(self):
_, entity_id = self.asset('Myfans')
self.connection.execute("INSERT INTO asset_entity(asset_id,entity_id,role,source) VALUES(99999,?,'tag','legacy')", (entity_id,))
self.connection.commit()
baseline = self.connection.execute('PRAGMA foreign_key_check').fetchall()
manifest = self.db.parent / 'manifest.json'
args = SimpleNamespace(db=self.db, apply=True, backup=self.db.parent/'before.db', restore=None, manifest=manifest)
audit._write_ledger(args, {'rows':self.plan()})
payload = json.loads(manifest.read_text(encoding='utf-8'))
self.assertEqual(payload['status'], 'committed')
self.assertEqual(self.connection.execute('PRAGMA foreign_key_check').fetchall(), baseline)
args.backup = self.db.parent / 'repaired.db'
args.restore = manifest
args.manifest = None
audit._write_ledger(args, payload)
self.assertEqual(self.connection.execute('SELECT creator FROM asset').fetchone()[0], 'Myfans')
self.assertEqual(self.connection.execute('PRAGMA foreign_key_check').fetchall(), baseline)
if __name__ == '__main__':
unittest.main()