"""官方预告的精确作品身份、地址边界、固定成功缓存和短期缺失缓存。"""
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
import httpx
from peach.http import HttpResponse
from peach.jav_official_preview import dmm_preview, dmm_preview_url, javdb_preview, prestige_preview
from peach.javdb_public import PublicAPI, PublicSourceUnavailable
from peach.preview_cache import PreviewCache
from peach.remote_media_cache import cache_for
from peach.web_jav_detail import q_jav_detail
from peach.web_state import WebContract
from support.ledger import fresh_ledger
UUID = '69fa4c03-103c-482e-aec5-14f971ba4d7d'
VIDEO = 'https://www.prestige-av.com/api/media/6/9/' + UUID + '.mp4'
DMM_VIDEO = 'https://cc3001.dmm.co.jp/pv/sample/1rctd007644k.mp4'
class OfficialPreviewTests(unittest.TestCase):
def setUp(self):
for name in ('dmm_preview', 'javdb_preview'):
patcher = patch('peach.jav_official_preview.' + name, return_value='')
patcher.start()
self.addCleanup(patcher.stop)
def test_dmm_search_reuses_exact_identity_and_declared_preview(self):
requests = []
def transport(request, *_):
body = json.loads(request.body)
requests.append(body)
if 'queryWord' in body['variables']:
data = {'legacySearchPPV': {'result': {'contents': [{'id': '1rctd00765'}, {'id': '1rctd00764'}]}}}
elif body['variables']['id'] == 'rctd00764':
data = {'ppvContent': None}
else:
data = {'ppvContent': {'id': '1rctd00764', 'makerContentId': 'RCTD-764',
'sample2DMovie': {'highestMovieUrl': DMM_VIDEO}}}
return HttpResponse(200, {}, json.dumps({'data': data}).encode())
self.assertEqual(dmm_preview('RCTD-764', Path('unused'), transport=transport), DMM_VIDEO)
self.assertEqual([row['variables'].get('id') for row in requests], ['rctd00764', None, '1rctd00764'])
self.assertIn('sample2DMovie', requests[-1]['query'])
def test_dmm_rejects_identity_mismatch_and_unrecognised_preview_fields(self):
item = {'id': 'rctd00764', 'makerContentId': 'RCTD-764', 'sample2DMovie': {'highestMovieUrl': DMM_VIDEO}}
missing = dict(item)
del missing['sample2DMovie']
for value in [missing, {**item, 'id': 'another'}, {**item, 'makerContentId': 'RCTD-765'},
{**item, 'sample2DMovie': {'highestMovieUrl': 'https://private.invalid/sample.mp4'}}]:
with self.subTest(value=value), self.assertRaises(PublicSourceUnavailable):
dmm_preview('RCTD-764', Path('unused'), transport=Mock(return_value=HttpResponse(
200, {}, json.dumps({'data': {'ppvContent': value}}).encode())))
self.assertEqual(dmm_preview('RCTD-764', Path('unused'), transport=Mock(return_value=HttpResponse(
200, {}, json.dumps({'data': {'ppvContent': {**item, 'sample2DMovie': None}}}).encode()))), '')
def test_dmm_media_boundary_rejects_credentials_fragments_and_other_hosts(self):
self.assertEqual(dmm_preview_url(DMM_VIDEO), DMM_VIDEO)
for url in [DMM_VIDEO.replace('https:', 'http:'), DMM_VIDEO + '#fragment',
DMM_VIDEO.replace('cc3001.', 'cc3001.evil.'), DMM_VIDEO.replace('https://', 'https://user:pass@'),
DMM_VIDEO.replace('/pv/', '/private/'), DMM_VIDEO.replace('.mp4', '.jpg')]:
with self.subTest(url=url):
self.assertEqual(dmm_preview_url(url), '')
def test_dmm_response_without_product_field_is_unavailable(self):
with self.assertRaises(PublicSourceUnavailable):
dmm_preview('RCTD-764', Path('unused'), transport=Mock(return_value=HttpResponse(200, {}, b'{"data": {}}')))
def test_javdb_reads_only_the_matching_work_declared_dmm_video(self):
html = ('
番號:RCTD-764
'
f'')
transport = Mock(return_value=HttpResponse(200, {}, html.encode()))
self.assertEqual(javdb_preview('RCTD-764', 'ZN9Ad6', Path('unused'), transport=transport), DMM_VIDEO)
self.assertEqual(transport.call_count, 1)
with self.assertRaises(PublicSourceUnavailable):
javdb_preview('RCTD-765', 'ZN9Ad6', Path('unused'), transport=transport)
def test_javdb_can_identify_the_work_without_webui_identity(self):
search = 'RCTD-764'
detail = ('番號:RCTD-764
'
f'')
transport = Mock(side_effect=[HttpResponse(200, {}, search.encode()), HttpResponse(200, {}, detail.encode())])
self.assertEqual(javdb_preview('RCTD-764', None, Path('unused'), transport=transport), DMM_VIDEO)
self.assertEqual(transport.call_count, 2)
def test_javdb_detail_page_waits_on_the_process_wide_javdb_interval(self):
"""资料处理刚问过 javdb.com,点开预告这一页排在同一条 3 秒队里,不另起一份间隔。"""
from peach import library_processing
from peach.scripting import HostLimiter
now, slept = [50.0], []
def sleep(delay):
slept.append(delay)
now[0] += delay
shared = HostLimiter(library_processing.SOURCE_INTERVALS, default_interval=2.0,
clock=lambda: now[0], sleeper=sleep)
html = ('番號:RCTD-764
'
f'')
inner = Mock(return_value=HttpResponse(200, {}, html.encode()))
with patch.object(library_processing, 'SOURCE_LIMITER', shared), \
patch('peach.jav_official_preview.SourceTransport', return_value=inner):
shared.wait('https://javdb.com/v/Other')
self.assertEqual(javdb_preview('RCTD-764', 'ZN9Ad6', Path('unused')), DMM_VIDEO)
self.assertEqual(slept, [3.0])
inner.close.assert_called_once()
def test_preview_source_order_and_webui_fallback(self):
mirror = 'https://jdforrepam.com/api/v1/movies/ttm3u8/preview/sample/720p.m3u8'
for dmm, javdb, expected in [(DMM_VIDEO, '', DMM_VIDEO), ('', DMM_VIDEO, DMM_VIDEO), ('', '', mirror),
(PublicSourceUnavailable('DMM 未取得'), '', mirror)]:
with self.subTest(dmm=dmm, javdb=javdb), tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
contract = WebContract(fresh_ledger(root), cover_root=root / 'covers')
self.addCleanup(contract.stop_background_jobs)
api = PublicAPI()
api.movie = Mock(return_value={'number': 'RCTD-764', 'id': 'ZN9Ad6', 'preview_video_url': mirror})
with patch('peach.web_jav_detail.PUBLIC_API', api), \
patch('peach.jav_official_preview.dmm_preview', side_effect=dmm if isinstance(dmm, Exception) else None,
return_value=dmm) as official, \
patch('peach.jav_official_preview.javdb_preview', return_value=javdb) as page:
payload = q_jav_detail(contract, {'code': 'RCTD-764'})
ref = payload['preview_video_url'].rsplit('/', 1)[-1]
self.assertEqual(PreviewCache(cache_for(contract)).source('RCTD-764', ref)['url'], expected)
official.assert_called_once()
self.assertEqual(page.call_count, 0 if dmm == DMM_VIDEO else 1)
def test_dmm_preview_cache_is_separate_and_rechecks_daily(self):
now = [1791936000.0]
api = PublicAPI(clock=lambda: now[0], wallclock=lambda: now[0])
fetch = Mock(return_value=DMM_VIDEO)
for _ in range(2):
self.assertEqual(api.official_preview('RCTD-764', fetch, provider='dmm', released='2020-01-01')['url'], DMM_VIDEO)
self.assertEqual(fetch.call_count, 1)
now[0] += 86400
api.official_preview('RCTD-764', fetch, provider='dmm', released='2020-01-01')
self.assertEqual(fetch.call_count, 2)
other = Mock(return_value=VIDEO)
self.assertEqual(api.official_preview('RCTD-764', other, provider='prestige')['url'], VIDEO)
other.assert_called_once()
def test_official_preview_is_available_when_webui_metadata_is_unavailable(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
contract = WebContract(fresh_ledger(root), cover_root=root / 'covers')
self.addCleanup(contract.stop_background_jobs)
api = PublicAPI()
api.movie = Mock(side_effect=PublicSourceUnavailable('WebUI 未取得'))
with patch('peach.web_jav_detail.PUBLIC_API', api), \
patch('peach.jav_official_preview.dmm_preview', return_value=DMM_VIDEO):
payload = q_jav_detail(contract, {'code': 'RCTD-764'})
self.assertEqual(payload['state'], 'partial')
self.assertEqual(payload['preview_state'], 'ready')
self.assertTrue(payload['preview_video_url'].startswith('/jav-preview/RCTD-764/'))
def test_dmm_preview_precedes_webui(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
contract = WebContract(fresh_ledger(root), cover_root=root / 'covers')
self.addCleanup(contract.stop_background_jobs)
api = PublicAPI()
api.movie = Mock(return_value={'number': 'RCTD-764', 'id': 'ZN9Ad6', 'maker_name': 'ROCKET',
'preview_video_url': 'https://jdforrepam.com/api/v1/movies/ttm3u8/preview/sample/720p.m3u8'})
with patch('peach.web_jav_detail.PUBLIC_API', api), \
patch.object(api, 'official_preview', return_value={'url': DMM_VIDEO}) as official:
payload = q_jav_detail(contract, {'code': 'RCTD-764'})
ref = payload['preview_video_url'].rsplit('/', 1)[-1]
self.assertEqual(PreviewCache(cache_for(contract)).source('RCTD-764', ref)['url'], DMM_VIDEO)
self.assertEqual(official.call_args.kwargs['provider'], 'dmm')
def transport(self, product):
return Mock(side_effect=[HttpResponse(200, {}, json.dumps({'parentProduct': {'uuid': UUID}}).encode()),
HttpResponse(200, {}, json.dumps(product).encode())])
def product(self, **values):
return {'uuid': UUID, 'sku': [{'deliveryItemId': 'EXM-001'}],
'movie': {'path': '6/9/' + UUID + '.mp4', 'mimeType': 'video/mp4', 'isLimited': 0}, **values}
def test_exact_sku_provides_declared_public_mp4_without_media_download(self):
transport = self.transport(self.product())
self.assertEqual(prestige_preview('EXM-001', Path('unused'), transport=transport), VIDEO)
self.assertEqual(transport.call_count, 2)
self.assertTrue(transport.call_args_list[0].args[0].url.endswith('/api/sku/item/EXM-001'))
self.assertTrue(transport.call_args_list[1].args[0].url.endswith('/api/product/' + UUID))
def test_undeclared_fields_wrong_identity_and_private_movies_are_unavailable(self):
missing = self.product()
del missing['movie']
for product in [missing, self.product(uuid='another'), self.product(sku=[{'deliveryItemId': 'EXM-002'}]),
self.product(movie={'path': '../private.mp4', 'mimeType': 'video/mp4', 'isLimited': 0}),
self.product(movie={'path': '6/9/' + UUID + '.mp4', 'mimeType': 'video/mp4', 'isLimited': 1})]:
with self.subTest(product=product), self.assertRaises(PublicSourceUnavailable):
prestige_preview('EXM-001', Path('unused'), transport=self.transport(product))
def test_source_absence_is_distinct_from_transport_failure(self):
self.assertEqual(prestige_preview('EXM-001', Path('unused'), transport=self.transport(self.product(movie=None))), '')
offline, slept = Mock(side_effect=httpx.ConnectError('offline')), []
# 连接失败按网络退避重试后才认定来源不可用;退避只记下时长,不真睡。
with patch('peach.jav_cover_fetch._sleep_within', side_effect=lambda delay, _: slept.append(delay)), \
self.assertRaises(PublicSourceUnavailable):
prestige_preview('EXM-001', Path('unused'), transport=offline)
self.assertEqual(slept, [2, 4, 6, 8])
self.assertEqual(offline.call_count, 5)
def test_empty_preview_rechecks_daily_and_good_old_preview_survives_restart(self):
with tempfile.TemporaryDirectory() as directory:
now = [1791936000.0]
root = Path(directory).resolve()
api = PublicAPI(cache_root=root, clock=lambda: now[0], wallclock=lambda: now[0])
fetch = Mock(return_value='')
self.assertEqual(api.official_preview('EXM-001', fetch, released='2020-01-01')['url'], '')
api.official_preview('EXM-001', fetch, released='2020-01-01')
self.assertEqual(fetch.call_count, 1)
now[0] += 86400
fetch.return_value = VIDEO
self.assertEqual(api.official_preview('EXM-001', fetch, released='2020-01-01')['url'], VIDEO)
self.assertEqual(fetch.call_count, 2)
now[0] += 86400 * 400
restarted = PublicAPI(cache_root=root, clock=lambda: now[0], wallclock=lambda: now[0])
forbidden = Mock(side_effect=AssertionError('来源请求不可执行'))
self.assertEqual(restarted.official_preview('EXM-001', forbidden)['url'], VIDEO)
forbidden.assert_not_called()
def test_detail_projects_official_mp4_and_preserves_movie_metadata(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
contract = WebContract(fresh_ledger(root), cover_root=root / 'covers')
self.addCleanup(contract.stop_background_jobs)
movie = {'number': 'EXM-001', 'id': 'Exact', 'maker_name': 'PRESTIGE,プレステージ',
'release_date': '2020-01-01', 'preview_video_url': None}
api = PublicAPI()
api.movie = Mock(return_value=movie)
with patch('peach.web_jav_detail.PUBLIC_API', api), \
patch('peach.jav_official_preview.prestige_preview', return_value=VIDEO) as official:
payload = q_jav_detail(contract, {'code': 'EXM-001'})
again = q_jav_detail(contract, {'code': 'EXM-001'})
self.assertEqual(payload['preview_state'], 'ready')
self.assertEqual(payload['preview_video_url'], again['preview_video_url'])
self.assertFalse(payload['preview_video_url'].endswith('.m3u8'))
ref = payload['preview_video_url'].rsplit('/', 1)[-1]
self.assertEqual(PreviewCache(cache_for(contract)).source('EXM-001', ref), {'url': VIDEO, 'playlist': False, 'media_type': 'video/mp4'})
official.assert_called_once()
self.assertEqual(movie['preview_video_url'], None)
def test_failed_official_preview_keeps_detail_and_reports_retryable_state(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
contract = WebContract(fresh_ledger(root), cover_root=root / 'covers')
self.addCleanup(contract.stop_background_jobs)
api = PublicAPI()
api.movie = Mock(return_value={'number': 'EXM-001', 'id': 'Exact', 'maker_name': 'Prestige'})
with patch('peach.web_jav_detail.PUBLIC_API', api), \
patch('peach.jav_official_preview.prestige_preview', side_effect=PublicSourceUnavailable('预告连接未取得')):
payload = q_jav_detail(contract, {'code': 'EXM-001'})
self.assertEqual(payload['state'], 'ready')
self.assertEqual(payload['preview_state'], 'unavailable')
self.assertIn('预告连接未取得', payload['message'])