mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-31 20:34:48 +08:00
* fix(download): preserve protocol failures and isolate site updates * test: cover tracker token lifecycle and record dependency facts --------- Co-authored-by: jxxghp <jxxghp@gmail.com>
143 lines
6.2 KiB
Python
143 lines
6.2 KiB
Python
"""
|
|
种子缓存相关的应用用例。
|
|
"""
|
|
|
|
from typing import Any, Optional
|
|
|
|
from app.domain.context import MediaInfo, MusicInfo
|
|
from app.domain.media import is_music_media_source, normalize_music_type
|
|
from app.domain.meta.metamusic import MetaMusic
|
|
from app.domain.metainfo import MetaInfo
|
|
from app.foundation.crypto import HashUtils
|
|
from app.runtime.cache import async_cache_mutation
|
|
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource, MediaType, MusicTargetEntityType
|
|
|
|
|
|
class TorrentCacheRecognitionService:
|
|
"""
|
|
执行种子缓存条目的媒体重新识别用例。
|
|
"""
|
|
|
|
def __init__(self, torrents_chain: Any, media_chain: Any) -> None:
|
|
"""
|
|
初始化缓存和媒体识别依赖。
|
|
"""
|
|
self.torrents_chain = torrents_chain
|
|
self.media_chain = media_chain
|
|
|
|
async def execute(
|
|
self,
|
|
domain: str,
|
|
torrent_hash: str,
|
|
media_source: Optional[MediaSource] = None,
|
|
media_id: Optional[str] = None,
|
|
music_type: Optional[MusicTargetEntityType] = None,
|
|
) -> tuple[bool, str, Optional[dict[str, object]]]:
|
|
"""
|
|
重新识别缓存条目并持久化影视、音乐分离后的缓存。
|
|
|
|
返回值保持端点原有的成功标识、用户消息和响应数据三元组,便于旧插件
|
|
继续消费原始 HTTP 响应结构。
|
|
"""
|
|
cache_data = await self.torrents_chain.async_get_torrents()
|
|
if domain not in cache_data:
|
|
return False, f"站点 {domain} 缓存不存在", None
|
|
|
|
target_context = next(
|
|
(
|
|
context
|
|
for context in cache_data[domain]
|
|
if HashUtils.md5(
|
|
f"{context.torrent_info.title}{context.torrent_info.description}"
|
|
)
|
|
== torrent_hash
|
|
),
|
|
None,
|
|
)
|
|
if not target_context:
|
|
return False, "未找到指定的种子", None
|
|
|
|
existing_music_type = normalize_music_type(
|
|
getattr(target_context.media_info, "music_type", None), allow_artist=False
|
|
)
|
|
normalized_music_type = normalize_music_type(music_type, allow_artist=False)
|
|
if music_type is not None and not normalized_music_type:
|
|
return False, "音乐实体类型无效,仅支持 recording 或 album", None
|
|
|
|
is_music = (
|
|
getattr(target_context.media_info, "type", None) == MediaType.MUSIC
|
|
or isinstance(target_context.meta_info, MetaMusic)
|
|
or target_context.torrent_info.category
|
|
in (MediaType.MUSIC, MediaType.MUSIC.value, "music")
|
|
or is_music_media_source(media_source)
|
|
or normalized_music_type is not None
|
|
)
|
|
if is_music and media_source and not is_music_media_source(media_source):
|
|
return False, "音乐重新识别只能使用音乐元数据源", None
|
|
if is_music and not normalized_music_type:
|
|
normalized_music_type = existing_music_type or MUSIC_ENTITY_RECORDING
|
|
|
|
meta = (
|
|
target_context.meta_info
|
|
if is_music and isinstance(target_context.meta_info, MetaMusic)
|
|
else MetaMusic.parse_query(target_context.torrent_info.title)
|
|
if is_music
|
|
else MetaInfo(
|
|
title=target_context.torrent_info.title,
|
|
subtitle=target_context.torrent_info.description,
|
|
)
|
|
)
|
|
has_explicit_id = media_source is not None or media_id is not None
|
|
if has_explicit_id and (not media_source or not media_id):
|
|
return False, "媒体来源和媒体 ID 必须同时提供", None
|
|
if has_explicit_id:
|
|
mediainfo = await self.media_chain.async_recognize_media(
|
|
meta=meta,
|
|
media_source=media_source,
|
|
media_id=media_id,
|
|
mtype=MediaType.MUSIC if is_music else None,
|
|
music_type=normalized_music_type,
|
|
)
|
|
else:
|
|
mediainfo = await self.media_chain.async_recognize_by_meta(
|
|
meta,
|
|
media_source=media_source,
|
|
mtype=MediaType.MUSIC if is_music else None,
|
|
music_type=normalized_music_type,
|
|
)
|
|
|
|
if not mediainfo:
|
|
mediainfo = (
|
|
MusicInfo(music_type=normalized_music_type or MUSIC_ENTITY_RECORDING)
|
|
if is_music
|
|
else MediaInfo()
|
|
)
|
|
else:
|
|
mediainfo.clear()
|
|
async with async_cache_mutation("torrent-snapshots"):
|
|
# 远端识别期间不占写锁;提交时重读当前快照,仅修改本次目标。
|
|
latest = await self.torrents_chain.async_get_torrents()
|
|
current = next((context for context in latest.get(domain, [])
|
|
if HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}")
|
|
== torrent_hash), None)
|
|
if current is None:
|
|
return False, "种子缓存已刷新,目标条目不存在", None
|
|
current.media_info = mediainfo
|
|
video_cache, music_cache = self.torrents_chain.split_cache_contexts(latest)
|
|
video_file, music_file = self.torrents_chain.cache_files()
|
|
# 同一个资源不会跨分类移动;只写其所属缓存,避免无关文件失败或部分提交。
|
|
is_music_cache = current.torrent_info.category in (MediaType.MUSIC, MediaType.MUSIC.value)
|
|
cache, filename = (music_cache, music_file) if is_music_cache else (video_cache, video_file)
|
|
try:
|
|
await self.torrents_chain.async_save_cache(cache, filename, strict=True)
|
|
except Exception:
|
|
return False, "种子缓存保存失败,请稍后重试", None
|
|
return True, "重新识别完成", {
|
|
"media_name": mediainfo.title if mediainfo else "",
|
|
"media_year": mediainfo.year if mediainfo else "",
|
|
"media_type": mediainfo.type.value if mediainfo and mediainfo.type else "",
|
|
"media_source": getattr(mediainfo, "media_source", None),
|
|
"media_id": getattr(mediainfo, "media_id", None),
|
|
"music_type": getattr(mediainfo, "music_type", None),
|
|
}
|