mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-27 11:11:07 +08:00
fix: align media recognition fallback and shared reporting
Route title and path lookups through the fallback-aware entrypoints so auxiliary matches can reuse pre-assist keywords without forcing image fetches in lightweight flows. Also reduce noisy agent shutdown logging during cleanup.
This commit is contained in:
@@ -6,7 +6,8 @@ from typing import List, Optional, Type
|
|||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from app.agent.tools.base import MoviePilotTool, ToolChain
|
from app.agent.tools.base import MoviePilotTool
|
||||||
|
from app.chain.media import MediaChain
|
||||||
from app.chain.search import SearchChain
|
from app.chain.search import SearchChain
|
||||||
from app.chain.download import DownloadChain
|
from app.chain.download import DownloadChain
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@@ -275,7 +276,10 @@ class AddDownloadTool(MoviePilotTool):
|
|||||||
meta_info = MetaInfo(title=torrent_title, subtitle=torrent_description)
|
meta_info = MetaInfo(title=torrent_title, subtitle=torrent_description)
|
||||||
media_info = cached_context.media_info if cached_context.media_info else None
|
media_info = cached_context.media_info if cached_context.media_info else None
|
||||||
if not media_info:
|
if not media_info:
|
||||||
media_info = await ToolChain().async_recognize_media(meta=meta_info)
|
media_info = await MediaChain().async_recognize_by_meta(
|
||||||
|
meta_info,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not media_info:
|
if not media_info:
|
||||||
failed_messages.append(f"{torrent_input} 无法识别媒体信息")
|
failed_messages.append(f"{torrent_input} 无法识别媒体信息")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ class RecognizeMediaTool(MoviePilotTool):
|
|||||||
"message": "文件路径不能为空"
|
"message": "文件路径不能为空"
|
||||||
}, ensure_ascii=False)
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
context = await media_chain.async_recognize_by_path(path)
|
context = await media_chain.async_recognize_by_path(
|
||||||
|
path,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if context:
|
if context:
|
||||||
return self._format_context_result(context, "文件")
|
return self._format_context_result(context, "文件")
|
||||||
else:
|
else:
|
||||||
@@ -73,7 +76,10 @@ class RecognizeMediaTool(MoviePilotTool):
|
|||||||
elif title:
|
elif title:
|
||||||
# 种子标题识别
|
# 种子标题识别
|
||||||
metainfo = MetaInfo(title, subtitle)
|
metainfo = MetaInfo(title, subtitle)
|
||||||
mediainfo = await media_chain.async_recognize_by_meta(metainfo)
|
mediainfo = await media_chain.async_recognize_by_meta(
|
||||||
|
metainfo,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
context = Context(meta_info=metainfo, media_info=mediainfo)
|
context = Context(meta_info=metainfo, media_info=mediainfo)
|
||||||
return self._format_context_result(context, "种子")
|
return self._format_context_result(context, "种子")
|
||||||
|
|||||||
@@ -76,12 +76,17 @@ def add(
|
|||||||
# 元数据
|
# 元数据
|
||||||
metainfo = MetaInfo(title=torrent_in.title, subtitle=torrent_in.description)
|
metainfo = MetaInfo(title=torrent_in.title, subtitle=torrent_in.description)
|
||||||
# 媒体信息
|
# 媒体信息
|
||||||
mediainfo = MediaChain().select_recognize_source(
|
if tmdbid or doubanid:
|
||||||
log_name=torrent_in.title,
|
mediainfo = MediaChain().recognize_media(
|
||||||
log_context=torrent_in.title,
|
meta=metainfo,
|
||||||
native_fn=lambda: MediaChain().recognize_media(meta=metainfo, tmdbid=tmdbid, doubanid=doubanid),
|
tmdbid=tmdbid,
|
||||||
plugin_fn=lambda: MediaChain().recognize_help(title=torrent_in.title, org_meta=metainfo)
|
doubanid=doubanid,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
mediainfo = MediaChain().recognize_by_meta(
|
||||||
|
metainfo,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
return schemas.Response(success=False, message="无法识别媒体信息")
|
return schemas.Response(success=False, message="无法识别媒体信息")
|
||||||
# 种子信息
|
# 种子信息
|
||||||
|
|||||||
@@ -202,7 +202,11 @@ async def seasons(mediaid: Optional[str] = None,
|
|||||||
meta = MetaInfo(title)
|
meta = MetaInfo(title)
|
||||||
if year:
|
if year:
|
||||||
meta.year = year
|
meta.year = year
|
||||||
mediainfo = await MediaChain().async_recognize_media(meta, mtype=MediaType.TV)
|
meta.type = MediaType.TV
|
||||||
|
mediainfo = await MediaChain().async_recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
||||||
seasons_info = await TmdbChain().async_tmdb_seasons(tmdbid=mediainfo.tmdb_id)
|
seasons_info = await TmdbChain().async_tmdb_seasons(tmdbid=mediainfo.tmdb_id)
|
||||||
@@ -261,7 +265,10 @@ async def detail(mediaid: str, type_name: str, title: Optional[str] = None, year
|
|||||||
meta.year = year
|
meta.year = year
|
||||||
if mtype:
|
if mtype:
|
||||||
meta.type = mtype
|
meta.type = mtype
|
||||||
mediainfo = await mediachain.async_recognize_media(meta=meta)
|
mediainfo = await mediachain.async_recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
# 识别
|
# 识别
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
await mediachain.async_obtain_images(mediainfo)
|
await mediachain.async_obtain_images(mediainfo)
|
||||||
|
|||||||
@@ -255,7 +255,10 @@ async def search_by_id_stream(request: Request,
|
|||||||
if media_season:
|
if media_season:
|
||||||
meta.type = MediaType.TV
|
meta.type = MediaType.TV
|
||||||
meta.begin_season = media_season
|
meta.begin_season = media_season
|
||||||
mediainfo = await media_chain.async_recognize_media(meta=meta)
|
mediainfo = await media_chain.async_recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
||||||
torrents = search_chain.async_search_by_id_stream(tmdbid=mediainfo.tmdb_id,
|
torrents = search_chain.async_search_by_id_stream(tmdbid=mediainfo.tmdb_id,
|
||||||
@@ -388,7 +391,10 @@ async def search_by_id(mediaid: str,
|
|||||||
if media_season:
|
if media_season:
|
||||||
meta.type = MediaType.TV
|
meta.type = MediaType.TV
|
||||||
meta.begin_season = media_season
|
meta.begin_season = media_season
|
||||||
mediainfo = await media_chain.async_recognize_media(meta=meta)
|
mediainfo = await media_chain.async_recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
||||||
torrents = await search_chain.async_search_by_id(tmdbid=mediainfo.tmdb_id, mtype=media_type,
|
torrents = await search_chain.async_search_by_id(tmdbid=mediainfo.tmdb_id, mtype=media_type,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from starlette.responses import FileResponse, Response
|
from starlette.responses import FileResponse, Response
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
|
from app.chain.media import MediaChain
|
||||||
from app.chain.storage import StorageChain
|
from app.chain.storage import StorageChain
|
||||||
from app.chain.transfer import TransferChain
|
from app.chain.transfer import TransferChain
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@@ -199,7 +200,10 @@ def rename(fileitem: schemas.FileItem,
|
|||||||
continue
|
continue
|
||||||
sub_path = Path(f"{fileitem.path}{sub_file.name}")
|
sub_path = Path(f"{fileitem.path}{sub_file.name}")
|
||||||
meta = MetaInfoPath(sub_path)
|
meta = MetaInfoPath(sub_path)
|
||||||
mediainfo = transferchain.recognize_media(meta)
|
mediainfo = MediaChain().recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
progress.end()
|
progress.end()
|
||||||
return schemas.Response(success=False, message=f"{sub_path.name} 未识别到媒体信息")
|
return schemas.Response(success=False, message=f"{sub_path.name} 未识别到媒体信息")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Header, Request, Re
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
|
from app.chain.media import MediaChain
|
||||||
from app.chain.mediaserver import MediaServerChain
|
from app.chain.mediaserver import MediaServerChain
|
||||||
from app.chain.search import SearchChain
|
from app.chain.search import SearchChain
|
||||||
from app.chain.system import SystemChain
|
from app.chain.system import SystemChain
|
||||||
@@ -785,7 +786,10 @@ def ruletest(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 根据标题查询媒体信息
|
# 根据标题查询媒体信息
|
||||||
media_info = SearchChain().recognize_media(MetaInfo(title=title, subtitle=subtitle))
|
media_info = MediaChain().recognize_by_meta(
|
||||||
|
MetaInfo(title=title, subtitle=subtitle),
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not media_info:
|
if not media_info:
|
||||||
return schemas.Response(success=False, message="未识别到媒体信息!")
|
return schemas.Response(success=False, message="未识别到媒体信息!")
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ def query_name(path: str, filetype: str,
|
|||||||
:param _: Token校验
|
:param _: Token校验
|
||||||
"""
|
"""
|
||||||
meta = MetaInfoPath(Path(path))
|
meta = MetaInfoPath(Path(path))
|
||||||
mediainfo = MediaChain().recognize_media(meta)
|
mediainfo = MediaChain().recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
return schemas.Response(success=False, message="未识别到媒体信息")
|
return schemas.Response(success=False, message="未识别到媒体信息")
|
||||||
new_path = TransferChain().recommend_name(meta=meta, mediainfo=mediainfo)
|
new_path = TransferChain().recommend_name(meta=meta, mediainfo=mediainfo)
|
||||||
|
|||||||
@@ -548,8 +548,12 @@ def arr_series_lookup(term: str, _: Annotated[str, Depends(verify_apikey)], db:
|
|||||||
seas = list(range(1, int(sea_num) + 1))
|
seas = list(range(1, int(sea_num) + 1))
|
||||||
|
|
||||||
# 根据TVDB查询媒体信息
|
# 根据TVDB查询媒体信息
|
||||||
mediainfo = MediaChain().recognize_media(meta=MetaInfo(tvdbinfo.get('name')),
|
meta = MetaInfo(tvdbinfo.get('name'))
|
||||||
mtype=MediaType.TV)
|
meta.type = MediaType.TV
|
||||||
|
mediainfo = MediaChain().recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
continue
|
continue
|
||||||
# 查询是否存在
|
# 查询是否存在
|
||||||
|
|||||||
@@ -465,10 +465,12 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
bangumiid: Optional[int] = None,
|
bangumiid: Optional[int] = None,
|
||||||
episode_group: Optional[str] = None,
|
episode_group: Optional[str] = None,
|
||||||
cache: bool = True,
|
cache: bool = True,
|
||||||
|
share_meta: MetaBase = None,
|
||||||
) -> Optional[MediaInfo]:
|
) -> Optional[MediaInfo]:
|
||||||
"""
|
"""
|
||||||
识别媒体信息,不含Fanart图片
|
识别媒体信息,不含Fanart图片
|
||||||
:param meta: 识别的元数据
|
:param meta: 识别的元数据
|
||||||
|
:param share_meta: 共享识别查询/上报使用的原始元数据
|
||||||
:param mtype: 识别的媒体类型,与tmdbid配套
|
:param mtype: 识别的媒体类型,与tmdbid配套
|
||||||
:param tmdbid: tmdbid
|
:param tmdbid: tmdbid
|
||||||
:param doubanid: 豆瓣ID
|
:param doubanid: 豆瓣ID
|
||||||
@@ -488,6 +490,7 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
bangumiid = None
|
bangumiid = None
|
||||||
elif not mtype and meta and meta.type in [MediaType.TV, MediaType.MOVIE]:
|
elif not mtype and meta and meta.type in [MediaType.TV, MediaType.MOVIE]:
|
||||||
mtype = meta.type
|
mtype = meta.type
|
||||||
|
share_query_meta = share_meta or meta
|
||||||
share_helper = MediaRecognizeShareHelper()
|
share_helper = MediaRecognizeShareHelper()
|
||||||
with fresh(not cache):
|
with fresh(not cache):
|
||||||
mediainfo = self.run_module(
|
mediainfo = self.run_module(
|
||||||
@@ -502,12 +505,22 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
)
|
)
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
if not mediainfo.recognize_cache_hit:
|
if not mediainfo.recognize_cache_hit:
|
||||||
share_helper.report(meta=meta, mediainfo=mediainfo)
|
share_helper.report(
|
||||||
|
meta=meta,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
keyword_meta=share_query_meta,
|
||||||
|
)
|
||||||
return mediainfo
|
return mediainfo
|
||||||
|
|
||||||
if self._can_use_media_recognize_share(meta, tmdbid, doubanid, bangumiid):
|
if self._can_use_media_recognize_share(
|
||||||
|
share_query_meta, tmdbid, doubanid, bangumiid
|
||||||
|
):
|
||||||
shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
|
shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
|
||||||
shared_item = share_helper.query(meta=meta, mtype=mtype)
|
shared_item = share_helper.query(
|
||||||
|
meta=meta,
|
||||||
|
mtype=mtype,
|
||||||
|
keyword_meta=share_query_meta,
|
||||||
|
)
|
||||||
shared_params = share_helper.to_recognize_params(shared_item)
|
shared_params = share_helper.to_recognize_params(shared_item)
|
||||||
if shared_params:
|
if shared_params:
|
||||||
with fresh(not cache):
|
with fresh(not cache):
|
||||||
@@ -535,10 +548,12 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
bangumiid: Optional[int] = None,
|
bangumiid: Optional[int] = None,
|
||||||
episode_group: Optional[str] = None,
|
episode_group: Optional[str] = None,
|
||||||
cache: bool = True,
|
cache: bool = True,
|
||||||
|
share_meta: MetaBase = None,
|
||||||
) -> Optional[MediaInfo]:
|
) -> Optional[MediaInfo]:
|
||||||
"""
|
"""
|
||||||
识别媒体信息,不含Fanart图片(异步版本)
|
识别媒体信息,不含Fanart图片(异步版本)
|
||||||
:param meta: 识别的元数据
|
:param meta: 识别的元数据
|
||||||
|
:param share_meta: 共享识别查询/上报使用的原始元数据
|
||||||
:param mtype: 识别的媒体类型,与tmdbid配套
|
:param mtype: 识别的媒体类型,与tmdbid配套
|
||||||
:param tmdbid: tmdbid
|
:param tmdbid: tmdbid
|
||||||
:param doubanid: 豆瓣ID
|
:param doubanid: 豆瓣ID
|
||||||
@@ -558,6 +573,7 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
bangumiid = None
|
bangumiid = None
|
||||||
elif not mtype and meta and meta.type in [MediaType.TV, MediaType.MOVIE]:
|
elif not mtype and meta and meta.type in [MediaType.TV, MediaType.MOVIE]:
|
||||||
mtype = meta.type
|
mtype = meta.type
|
||||||
|
share_query_meta = share_meta or meta
|
||||||
share_helper = MediaRecognizeShareHelper()
|
share_helper = MediaRecognizeShareHelper()
|
||||||
async with async_fresh(not cache):
|
async with async_fresh(not cache):
|
||||||
mediainfo = await self.async_run_module(
|
mediainfo = await self.async_run_module(
|
||||||
@@ -572,12 +588,22 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
)
|
)
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
if not mediainfo.recognize_cache_hit:
|
if not mediainfo.recognize_cache_hit:
|
||||||
await share_helper.async_report(meta=meta, mediainfo=mediainfo)
|
await share_helper.async_report(
|
||||||
|
meta=meta,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
keyword_meta=share_query_meta,
|
||||||
|
)
|
||||||
return mediainfo
|
return mediainfo
|
||||||
|
|
||||||
if self._can_use_media_recognize_share(meta, tmdbid, doubanid, bangumiid):
|
if self._can_use_media_recognize_share(
|
||||||
|
share_query_meta, tmdbid, doubanid, bangumiid
|
||||||
|
):
|
||||||
shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
|
shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
|
||||||
shared_item = await share_helper.async_query(meta=meta, mtype=mtype)
|
shared_item = await share_helper.async_query(
|
||||||
|
meta=meta,
|
||||||
|
mtype=mtype,
|
||||||
|
keyword_meta=share_query_meta,
|
||||||
|
)
|
||||||
shared_params = share_helper.to_recognize_params(shared_item)
|
shared_params = share_helper.to_recognize_params(shared_item)
|
||||||
if shared_params:
|
if shared_params:
|
||||||
async with async_fresh(not cache):
|
async with async_fresh(not cache):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
@@ -447,39 +448,83 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
return mediainfo
|
return mediainfo
|
||||||
|
|
||||||
def recognize_by_meta(
|
def recognize_by_meta(
|
||||||
self, metainfo: MetaBase, episode_group: Optional[str] = None
|
self,
|
||||||
|
metainfo: MetaBase,
|
||||||
|
episode_group: Optional[str] = None,
|
||||||
|
obtain_images: bool = True,
|
||||||
) -> Optional[MediaInfo]:
|
) -> Optional[MediaInfo]:
|
||||||
"""
|
"""
|
||||||
根据主副标题识别媒体信息
|
根据主副标题识别媒体信息
|
||||||
"""
|
"""
|
||||||
|
mediainfo = self._recognize_with_fallback_by_meta(
|
||||||
|
metainfo=metainfo,
|
||||||
|
episode_group=episode_group,
|
||||||
|
obtain_images=obtain_images,
|
||||||
|
)
|
||||||
|
if not mediainfo:
|
||||||
|
logger.warn(f"{metainfo.title} 未识别到媒体信息")
|
||||||
|
return mediainfo
|
||||||
|
|
||||||
|
def _recognize_with_fallback_by_meta(
|
||||||
|
self,
|
||||||
|
metainfo: MetaBase,
|
||||||
|
episode_group: Optional[str] = None,
|
||||||
|
obtain_images: bool = False,
|
||||||
|
) -> Optional[MediaInfo]:
|
||||||
|
"""
|
||||||
|
根据标题识别媒体信息,必要时回退到辅助识别。
|
||||||
|
"""
|
||||||
|
if not metainfo:
|
||||||
|
return None
|
||||||
title = metainfo.title
|
title = metainfo.title
|
||||||
|
share_meta = deepcopy(metainfo)
|
||||||
|
|
||||||
|
def native_recognize() -> Optional[MediaInfo]:
|
||||||
|
return self.recognize_media(
|
||||||
|
meta=metainfo,
|
||||||
|
share_meta=share_meta,
|
||||||
|
episode_group=episode_group,
|
||||||
|
)
|
||||||
|
|
||||||
|
def plugin_recognize() -> Optional[MediaInfo]:
|
||||||
|
return self.recognize_help(
|
||||||
|
title=title,
|
||||||
|
org_meta=metainfo,
|
||||||
|
share_meta=share_meta,
|
||||||
|
episode_group=episode_group,
|
||||||
|
)
|
||||||
|
|
||||||
# 按 config 中设置的识别顺序识别
|
# 按 config 中设置的识别顺序识别
|
||||||
mediainfo = self.select_recognize_source(
|
mediainfo = self.select_recognize_source(
|
||||||
log_name=title,
|
log_name=title,
|
||||||
log_context=title,
|
log_context=title,
|
||||||
native_fn=lambda: self.recognize_media(
|
native_fn=native_recognize,
|
||||||
meta=metainfo, episode_group=episode_group
|
plugin_fn=plugin_recognize,
|
||||||
),
|
|
||||||
plugin_fn=lambda: self.recognize_help(title=title, org_meta=metainfo),
|
|
||||||
)
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.warn(f"{title} 未识别到媒体信息")
|
|
||||||
return None
|
return None
|
||||||
# 识别成功
|
# 识别成功
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}"
|
f"{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}"
|
||||||
)
|
)
|
||||||
# 更新媒体图片
|
if obtain_images:
|
||||||
self.obtain_images(mediainfo=mediainfo)
|
self.obtain_images(mediainfo=mediainfo)
|
||||||
# 返回上下文
|
|
||||||
return mediainfo
|
return mediainfo
|
||||||
|
|
||||||
def recognize_help(self, title: str, org_meta: MetaBase) -> Optional[MediaInfo]:
|
def recognize_help(
|
||||||
|
self,
|
||||||
|
title: str,
|
||||||
|
org_meta: MetaBase,
|
||||||
|
share_meta: MetaBase = None,
|
||||||
|
episode_group: Optional[str] = None,
|
||||||
|
) -> Optional[MediaInfo]:
|
||||||
"""
|
"""
|
||||||
请求辅助识别,返回媒体信息
|
请求辅助识别,返回媒体信息
|
||||||
|
|
||||||
:param title: 标题
|
:param title: 标题
|
||||||
:param org_meta: 原始元数据
|
:param org_meta: 原始元数据
|
||||||
|
:param share_meta: 共享识别查询/上报使用的原始元数据
|
||||||
|
:param episode_group: 剧集组
|
||||||
"""
|
"""
|
||||||
# 发送请求事件,等待结果
|
# 发送请求事件,等待结果
|
||||||
result: Event = eventmanager.send_event(
|
result: Event = eventmanager.send_event(
|
||||||
@@ -521,10 +566,17 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
if org_meta.begin_season is not None or org_meta.begin_episode is not None:
|
if org_meta.begin_season is not None or org_meta.begin_episode is not None:
|
||||||
org_meta.type = MediaType.TV
|
org_meta.type = MediaType.TV
|
||||||
# 重新识别
|
# 重新识别
|
||||||
return self.recognize_media(meta=org_meta)
|
return self.recognize_media(
|
||||||
|
meta=org_meta,
|
||||||
|
share_meta=share_meta,
|
||||||
|
episode_group=episode_group,
|
||||||
|
)
|
||||||
|
|
||||||
def recognize_by_path(
|
def recognize_by_path(
|
||||||
self, path: str, episode_group: Optional[str] = None
|
self,
|
||||||
|
path: str,
|
||||||
|
episode_group: Optional[str] = None,
|
||||||
|
obtain_images: bool = True,
|
||||||
) -> Optional[Context]:
|
) -> Optional[Context]:
|
||||||
"""
|
"""
|
||||||
根据文件路径识别媒体信息
|
根据文件路径识别媒体信息
|
||||||
@@ -533,23 +585,14 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
file_path = Path(path)
|
file_path = Path(path)
|
||||||
# 元数据
|
# 元数据
|
||||||
file_meta = MetaInfoPath(file_path)
|
file_meta = MetaInfoPath(file_path)
|
||||||
# 按 config 中设置的识别顺序识别
|
mediainfo = self._recognize_with_fallback_by_meta(
|
||||||
mediainfo = self.select_recognize_source(
|
metainfo=file_meta,
|
||||||
log_name=file_path.name,
|
episode_group=episode_group,
|
||||||
log_context=path,
|
obtain_images=obtain_images,
|
||||||
native_fn=lambda: self.recognize_media(
|
|
||||||
meta=file_meta, episode_group=episode_group
|
|
||||||
),
|
|
||||||
plugin_fn=lambda: self.recognize_help(title=path, org_meta=file_meta),
|
|
||||||
)
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.warn(f"{path} 未识别到媒体信息")
|
logger.warn(f"{path} 未识别到媒体信息")
|
||||||
return Context(meta_info=file_meta)
|
return Context(meta_info=file_meta)
|
||||||
logger.info(
|
|
||||||
f"{path} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}"
|
|
||||||
)
|
|
||||||
# 更新媒体图片
|
|
||||||
self.obtain_images(mediainfo=mediainfo)
|
|
||||||
# 返回上下文
|
# 返回上下文
|
||||||
return Context(meta_info=file_meta, media_info=mediainfo)
|
return Context(meta_info=file_meta, media_info=mediainfo)
|
||||||
|
|
||||||
@@ -1340,21 +1383,51 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
return mediainfo
|
return mediainfo
|
||||||
|
|
||||||
async def async_recognize_by_meta(
|
async def async_recognize_by_meta(
|
||||||
self, metainfo: MetaBase, episode_group: Optional[str] = None
|
self,
|
||||||
|
metainfo: MetaBase,
|
||||||
|
episode_group: Optional[str] = None,
|
||||||
|
obtain_images: bool = True,
|
||||||
) -> Optional[MediaInfo]:
|
) -> Optional[MediaInfo]:
|
||||||
"""
|
"""
|
||||||
根据主副标题识别媒体信息(异步版本)
|
根据主副标题识别媒体信息(异步版本)
|
||||||
"""
|
"""
|
||||||
title = metainfo.title
|
mediainfo = await self._async_recognize_with_fallback_by_meta(
|
||||||
|
metainfo=metainfo,
|
||||||
|
episode_group=episode_group,
|
||||||
|
obtain_images=obtain_images,
|
||||||
|
)
|
||||||
|
if not mediainfo:
|
||||||
|
logger.warn(f"{metainfo.title} 未识别到媒体信息")
|
||||||
|
return mediainfo
|
||||||
|
|
||||||
|
async def _async_recognize_with_fallback_by_meta(
|
||||||
|
self,
|
||||||
|
metainfo: MetaBase,
|
||||||
|
episode_group: Optional[str] = None,
|
||||||
|
obtain_images: bool = False,
|
||||||
|
) -> Optional[MediaInfo]:
|
||||||
|
"""
|
||||||
|
异步根据标题识别媒体信息,必要时回退到辅助识别。
|
||||||
|
"""
|
||||||
|
if not metainfo:
|
||||||
|
return None
|
||||||
|
title = metainfo.title
|
||||||
|
share_meta = deepcopy(metainfo)
|
||||||
|
|
||||||
# 定义识别函数
|
|
||||||
async def native_recognize():
|
async def native_recognize():
|
||||||
return await self.async_recognize_media(
|
return await self.async_recognize_media(
|
||||||
meta=metainfo, episode_group=episode_group
|
meta=metainfo,
|
||||||
|
share_meta=share_meta,
|
||||||
|
episode_group=episode_group,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def plugin_recognize():
|
async def plugin_recognize():
|
||||||
return await self.async_recognize_help(title=title, org_meta=metainfo)
|
return await self.async_recognize_help(
|
||||||
|
title=title,
|
||||||
|
org_meta=metainfo,
|
||||||
|
share_meta=share_meta,
|
||||||
|
episode_group=episode_group,
|
||||||
|
)
|
||||||
|
|
||||||
# 按 config 中设置的识别顺序识别
|
# 按 config 中设置的识别顺序识别
|
||||||
mediainfo = await self.async_select_recognize_source(
|
mediainfo = await self.async_select_recognize_source(
|
||||||
@@ -1364,25 +1437,28 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
plugin_fn=plugin_recognize,
|
plugin_fn=plugin_recognize,
|
||||||
)
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.warn(f"{title} 未识别到媒体信息")
|
|
||||||
return None
|
return None
|
||||||
# 识别成功
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}"
|
f"{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}"
|
||||||
)
|
)
|
||||||
# 更新媒体图片
|
if obtain_images:
|
||||||
await self.async_obtain_images(mediainfo=mediainfo)
|
await self.async_obtain_images(mediainfo=mediainfo)
|
||||||
# 返回上下文
|
|
||||||
return mediainfo
|
return mediainfo
|
||||||
|
|
||||||
async def async_recognize_help(
|
async def async_recognize_help(
|
||||||
self, title: str, org_meta: MetaBase
|
self,
|
||||||
|
title: str,
|
||||||
|
org_meta: MetaBase,
|
||||||
|
share_meta: MetaBase = None,
|
||||||
|
episode_group: Optional[str] = None,
|
||||||
) -> Optional[MediaInfo]:
|
) -> Optional[MediaInfo]:
|
||||||
"""
|
"""
|
||||||
请求辅助识别,返回媒体信息(异步版本)
|
请求辅助识别,返回媒体信息(异步版本)
|
||||||
|
|
||||||
:param title: 标题
|
:param title: 标题
|
||||||
:param org_meta: 原始元数据
|
:param org_meta: 原始元数据
|
||||||
|
:param share_meta: 共享识别查询/上报使用的原始元数据
|
||||||
|
:param episode_group: 剧集组
|
||||||
"""
|
"""
|
||||||
# 发送请求事件,等待结果
|
# 发送请求事件,等待结果
|
||||||
result: Event = await eventmanager.async_send_event(
|
result: Event = await eventmanager.async_send_event(
|
||||||
@@ -1424,10 +1500,17 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
if org_meta.begin_season or org_meta.begin_episode:
|
if org_meta.begin_season or org_meta.begin_episode:
|
||||||
org_meta.type = MediaType.TV
|
org_meta.type = MediaType.TV
|
||||||
# 重新识别
|
# 重新识别
|
||||||
return await self.async_recognize_media(meta=org_meta)
|
return await self.async_recognize_media(
|
||||||
|
meta=org_meta,
|
||||||
|
share_meta=share_meta,
|
||||||
|
episode_group=episode_group,
|
||||||
|
)
|
||||||
|
|
||||||
async def async_recognize_by_path(
|
async def async_recognize_by_path(
|
||||||
self, path: str, episode_group: Optional[str] = None
|
self,
|
||||||
|
path: str,
|
||||||
|
episode_group: Optional[str] = None,
|
||||||
|
obtain_images: bool = True,
|
||||||
) -> Optional[Context]:
|
) -> Optional[Context]:
|
||||||
"""
|
"""
|
||||||
根据文件路径识别媒体信息(异步版本)
|
根据文件路径识别媒体信息(异步版本)
|
||||||
@@ -1436,31 +1519,14 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
file_path = Path(path)
|
file_path = Path(path)
|
||||||
# 元数据
|
# 元数据
|
||||||
file_meta = MetaInfoPath(file_path)
|
file_meta = MetaInfoPath(file_path)
|
||||||
|
mediainfo = await self._async_recognize_with_fallback_by_meta(
|
||||||
# 定义识别函数
|
metainfo=file_meta,
|
||||||
async def native_recognize():
|
episode_group=episode_group,
|
||||||
return await self.async_recognize_media(
|
obtain_images=obtain_images,
|
||||||
meta=file_meta, episode_group=episode_group
|
|
||||||
)
|
|
||||||
|
|
||||||
async def plugin_recognize():
|
|
||||||
return await self.async_recognize_help(title=path, org_meta=file_meta)
|
|
||||||
|
|
||||||
# 按 config 中设置的识别顺序识别
|
|
||||||
mediainfo = await self.async_select_recognize_source(
|
|
||||||
log_name=file_path.name,
|
|
||||||
log_context=path,
|
|
||||||
native_fn=native_recognize,
|
|
||||||
plugin_fn=plugin_recognize,
|
|
||||||
)
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.warn(f"{path} 未识别到媒体信息")
|
logger.warn(f"{path} 未识别到媒体信息")
|
||||||
return Context(meta_info=file_meta)
|
return Context(meta_info=file_meta)
|
||||||
logger.info(
|
|
||||||
f"{path} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}"
|
|
||||||
)
|
|
||||||
# 更新媒体图片
|
|
||||||
await self.async_obtain_images(mediainfo=mediainfo)
|
|
||||||
# 返回上下文
|
# 返回上下文
|
||||||
return Context(meta_info=file_meta, media_info=mediainfo)
|
return Context(meta_info=file_meta, media_info=mediainfo)
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,11 @@ class SubscribeChain(ChainBase):
|
|||||||
|
|
||||||
# 使用名称识别兜底
|
# 使用名称识别兜底
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
mediainfo = self.recognize_media(meta=metainfo, episode_group=episode_group)
|
mediainfo = MediaChain().recognize_by_meta(
|
||||||
|
metainfo,
|
||||||
|
episode_group=episode_group,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
|
|
||||||
# 识别失败
|
# 识别失败
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
@@ -371,7 +375,11 @@ class SubscribeChain(ChainBase):
|
|||||||
|
|
||||||
# 使用名称识别兜底
|
# 使用名称识别兜底
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
mediainfo = await self.async_recognize_media(meta=metainfo, episode_group=episode_group)
|
mediainfo = await MediaChain().async_recognize_by_meta(
|
||||||
|
metainfo,
|
||||||
|
episode_group=episode_group,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
|
|
||||||
# 识别失败
|
# 识别失败
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
@@ -827,7 +835,10 @@ class SubscribeChain(ChainBase):
|
|||||||
and not context.media_info.douban_id)) and context.media_recognize_fail_count < 3:
|
and not context.media_info.douban_id)) and context.media_recognize_fail_count < 3:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'尝试重新识别种子:{context.torrent_info.title},当前失败次数:{context.media_recognize_fail_count}/3')
|
f'尝试重新识别种子:{context.torrent_info.title},当前失败次数:{context.media_recognize_fail_count}/3')
|
||||||
re_mediainfo = self.recognize_media(meta=context.meta_info)
|
re_mediainfo = MediaChain().recognize_by_meta(
|
||||||
|
context.meta_info,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if re_mediainfo:
|
if re_mediainfo:
|
||||||
# 清理多余信息
|
# 清理多余信息
|
||||||
re_mediainfo.clear()
|
re_mediainfo.clear()
|
||||||
@@ -939,8 +950,11 @@ class SubscribeChain(ChainBase):
|
|||||||
# 更新元数据缓存
|
# 更新元数据缓存
|
||||||
_context.meta_info = torrent_meta
|
_context.meta_info = torrent_meta
|
||||||
# 重新识别媒体信息
|
# 重新识别媒体信息
|
||||||
torrent_mediainfo = self.recognize_media(meta=torrent_meta,
|
torrent_mediainfo = MediaChain().recognize_by_meta(
|
||||||
episode_group=subscribe.episode_group)
|
torrent_meta,
|
||||||
|
episode_group=subscribe.episode_group,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if torrent_mediainfo:
|
if torrent_mediainfo:
|
||||||
# 清理多余信息
|
# 清理多余信息
|
||||||
torrent_mediainfo.clear()
|
torrent_mediainfo.clear()
|
||||||
|
|||||||
@@ -278,7 +278,10 @@ class TorrentsChain(ChainBase):
|
|||||||
and torrent.category == MediaType.TV.value:
|
and torrent.category == MediaType.TV.value:
|
||||||
meta.type = MediaType.TV
|
meta.type = MediaType.TV
|
||||||
# 识别媒体信息
|
# 识别媒体信息
|
||||||
mediainfo: MediaInfo = MediaChain().recognize_by_meta(meta)
|
mediainfo: MediaInfo = MediaChain().recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.warn(f'{torrent.title} 未识别到媒体信息')
|
logger.warn(f'{torrent.title} 未识别到媒体信息')
|
||||||
# 存储空的媒体信息
|
# 存储空的媒体信息
|
||||||
|
|||||||
@@ -1289,7 +1289,10 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
mediainfo.category = download_history.media_category
|
mediainfo.category = download_history.media_category
|
||||||
else:
|
else:
|
||||||
# 识别媒体信息
|
# 识别媒体信息
|
||||||
mediainfo = MediaChain().recognize_by_meta(task.meta)
|
mediainfo = MediaChain().recognize_by_meta(
|
||||||
|
task.meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
|
|
||||||
# 更新媒体图片
|
# 更新媒体图片
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
@@ -2269,9 +2272,12 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
# 更新媒体图片
|
# 更新媒体图片
|
||||||
self.obtain_images(mediainfo=mediainfo)
|
self.obtain_images(mediainfo=mediainfo)
|
||||||
else:
|
else:
|
||||||
mediainfo = MediaChain().recognize_by_path(
|
recognize_context = MediaChain().recognize_by_path(
|
||||||
str(src_path), episode_group=history.episode_group
|
str(src_path),
|
||||||
|
episode_group=history.episode_group,
|
||||||
|
obtain_images=False,
|
||||||
)
|
)
|
||||||
|
mediainfo = recognize_context.media_info if recognize_context else None
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
return False, f"未识别到媒体信息,类型:{mtype.value},id:{mediaid}"
|
return False, f"未识别到媒体信息,类型:{mtype.value},id:{mediaid}"
|
||||||
# 重新执行整理
|
# 重新执行整理
|
||||||
|
|||||||
@@ -114,12 +114,15 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_query_params(
|
def _build_query_params(
|
||||||
cls, meta: Optional[MetaBase], mtype: Optional[MediaType] = None
|
cls,
|
||||||
|
meta: Optional[MetaBase],
|
||||||
|
mtype: Optional[MediaType] = None,
|
||||||
|
keyword_meta: Optional[MetaBase] = None,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
组装共享识别查询参数
|
组装共享识别查询参数
|
||||||
"""
|
"""
|
||||||
keyword = cls._extract_keyword(meta)
|
keyword = cls._extract_keyword(keyword_meta or meta)
|
||||||
if not keyword:
|
if not keyword:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -137,7 +140,10 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_report_payload(
|
def _build_report_payload(
|
||||||
cls, meta: Optional[MetaBase], mediainfo: Optional[MediaInfo]
|
cls,
|
||||||
|
meta: Optional[MetaBase],
|
||||||
|
mediainfo: Optional[MediaInfo],
|
||||||
|
keyword_meta: Optional[MetaBase] = None,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
组装共享识别上报载荷
|
组装共享识别上报载荷
|
||||||
@@ -145,7 +151,7 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
if not meta or not mediainfo:
|
if not meta or not mediainfo:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
keyword = cls._extract_keyword(meta)
|
keyword = cls._extract_keyword(keyword_meta or meta)
|
||||||
media_type = cls._extract_media_type(meta=meta, mediainfo=mediainfo)
|
media_type = cls._extract_media_type(meta=meta, mediainfo=mediainfo)
|
||||||
if not keyword or not media_type:
|
if not keyword or not media_type:
|
||||||
return None
|
return None
|
||||||
@@ -197,7 +203,12 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
"""
|
"""
|
||||||
return bool(settings.MEDIA_RECOGNIZE_SHARE)
|
return bool(settings.MEDIA_RECOGNIZE_SHARE)
|
||||||
|
|
||||||
def query(self, meta: Optional[MetaBase], mtype: Optional[MediaType] = None) -> Optional[dict]:
|
def query(
|
||||||
|
self,
|
||||||
|
meta: Optional[MetaBase],
|
||||||
|
mtype: Optional[MediaType] = None,
|
||||||
|
keyword_meta: Optional[MetaBase] = None,
|
||||||
|
) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
查询共享识别结果
|
查询共享识别结果
|
||||||
"""
|
"""
|
||||||
@@ -205,7 +216,11 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
api_url = self._build_api_url()
|
api_url = self._build_api_url()
|
||||||
params = self._build_query_params(meta=meta, mtype=mtype)
|
params = self._build_query_params(
|
||||||
|
meta=meta,
|
||||||
|
mtype=mtype,
|
||||||
|
keyword_meta=keyword_meta,
|
||||||
|
)
|
||||||
if not api_url or not params:
|
if not api_url or not params:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -236,7 +251,10 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
async def async_query(
|
async def async_query(
|
||||||
self, meta: Optional[MetaBase], mtype: Optional[MediaType] = None
|
self,
|
||||||
|
meta: Optional[MetaBase],
|
||||||
|
mtype: Optional[MediaType] = None,
|
||||||
|
keyword_meta: Optional[MetaBase] = None,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
异步查询共享识别结果
|
异步查询共享识别结果
|
||||||
@@ -245,7 +263,11 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
api_url = self._build_api_url()
|
api_url = self._build_api_url()
|
||||||
params = self._build_query_params(meta=meta, mtype=mtype)
|
params = self._build_query_params(
|
||||||
|
meta=meta,
|
||||||
|
mtype=mtype,
|
||||||
|
keyword_meta=keyword_meta,
|
||||||
|
)
|
||||||
if not api_url or not params:
|
if not api_url or not params:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -275,7 +297,12 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
logger.info(f"共享媒体识别命中:{params.get('keyword')} - {item}")
|
logger.info(f"共享媒体识别命中:{params.get('keyword')} - {item}")
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def report(self, meta: Optional[MetaBase], mediainfo: Optional[MediaInfo]) -> bool:
|
def report(
|
||||||
|
self,
|
||||||
|
meta: Optional[MetaBase],
|
||||||
|
mediainfo: Optional[MediaInfo],
|
||||||
|
keyword_meta: Optional[MetaBase] = None,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
上报共享识别结果
|
上报共享识别结果
|
||||||
"""
|
"""
|
||||||
@@ -283,7 +310,11 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
api_url = self._build_api_url()
|
api_url = self._build_api_url()
|
||||||
payload = self._build_report_payload(meta=meta, mediainfo=mediainfo)
|
payload = self._build_report_payload(
|
||||||
|
meta=meta,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
keyword_meta=keyword_meta,
|
||||||
|
)
|
||||||
if not api_url or not payload:
|
if not api_url or not payload:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -309,7 +340,10 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
return result.get("code") == 0
|
return result.get("code") == 0
|
||||||
|
|
||||||
async def async_report(
|
async def async_report(
|
||||||
self, meta: Optional[MetaBase], mediainfo: Optional[MediaInfo]
|
self,
|
||||||
|
meta: Optional[MetaBase],
|
||||||
|
mediainfo: Optional[MediaInfo],
|
||||||
|
keyword_meta: Optional[MetaBase] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
异步上报共享识别结果
|
异步上报共享识别结果
|
||||||
@@ -318,7 +352,11 @@ class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
api_url = self._build_api_url()
|
api_url = self._build_api_url()
|
||||||
payload = self._build_report_payload(meta=meta, mediainfo=mediainfo)
|
payload = self._build_report_payload(
|
||||||
|
meta=meta,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
keyword_meta=keyword_meta,
|
||||||
|
)
|
||||||
if not api_url or not payload:
|
if not api_url or not payload:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class AgentInitializer:
|
|||||||
logger.info("AI智能体管理器已关闭")
|
logger.info("AI智能体管理器已关闭")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"关闭AI智能体管理器时发生错误: {e}")
|
logger.debug(f"关闭AI智能体管理器时发生错误: {e}")
|
||||||
|
|
||||||
|
|
||||||
# 全局AI智能体初始化器实例
|
# 全局AI智能体初始化器实例
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ class AddDownloadAction(BaseAction):
|
|||||||
if not t.meta_info:
|
if not t.meta_info:
|
||||||
t.meta_info = MetaInfo(title=t.torrent_info.title, subtitle=t.torrent_info.description)
|
t.meta_info = MetaInfo(title=t.torrent_info.title, subtitle=t.torrent_info.description)
|
||||||
if not t.media_info:
|
if not t.media_info:
|
||||||
t.media_info = MediaChain().recognize_media(meta=t.meta_info)
|
t.media_info = MediaChain().recognize_by_meta(
|
||||||
|
t.meta_info,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not t.media_info:
|
if not t.media_info:
|
||||||
self._has_error = True
|
self._has_error = True
|
||||||
logger.warning(f"{t.torrent_info.title} 未识别到媒体信息,无法下载")
|
logger.warning(f"{t.torrent_info.title} 未识别到媒体信息,无法下载")
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ from typing import Optional
|
|||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from app.workflow.actions import BaseAction, ActionChain
|
from app.workflow.actions import BaseAction
|
||||||
|
from app.chain.media import MediaChain
|
||||||
from app.core.config import settings, global_vars
|
from app.core.config import settings, global_vars
|
||||||
from app.core.context import Context
|
from app.core.context import Context
|
||||||
from app.core.metainfo import MetaInfo
|
from app.core.metainfo import MetaInfo
|
||||||
@@ -98,7 +99,10 @@ class FetchRssAction(BaseAction):
|
|||||||
meta = MetaInfo(title=torrentinfo.title, subtitle=torrentinfo.description)
|
meta = MetaInfo(title=torrentinfo.title, subtitle=torrentinfo.description)
|
||||||
mediainfo = None
|
mediainfo = None
|
||||||
if params.match_media:
|
if params.match_media:
|
||||||
mediainfo = ActionChain().recognize_media(meta)
|
mediainfo = MediaChain().recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.warning(f"{torrentinfo.title} 未识别到媒体信息")
|
logger.warning(f"{torrentinfo.title} 未识别到媒体信息")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -72,7 +72,10 @@ class FetchTorrentsAction(BaseAction):
|
|||||||
continue
|
continue
|
||||||
# 识别媒体信息
|
# 识别媒体信息
|
||||||
if params.match_media:
|
if params.match_media:
|
||||||
torrent.media_info = searchchain.recognize_media(torrent.meta_info)
|
torrent.media_info = searchchain.recognize_by_meta(
|
||||||
|
torrent.meta_info,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not torrent.media_info:
|
if not torrent.media_info:
|
||||||
logger.warning(f"{torrent.torrent_info.title} 未识别到媒体信息")
|
logger.warning(f"{torrent.torrent_info.title} 未识别到媒体信息")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ class ScrapeFileAction(BaseAction):
|
|||||||
continue
|
continue
|
||||||
meta = MetaInfoPath(Path(fileitem.path))
|
meta = MetaInfoPath(Path(fileitem.path))
|
||||||
mediachain = MediaChain()
|
mediachain = MediaChain()
|
||||||
mediainfo = mediachain.recognize_media(meta)
|
mediainfo = mediachain.recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
_failed_count += 1
|
_failed_count += 1
|
||||||
logger.info(f"{fileitem.path} 未识别到媒体信息,无法刮削")
|
logger.info(f"{fileitem.path} 未识别到媒体信息,无法刮削")
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ sys.modules.setdefault("psutil", ModuleType("psutil"))
|
|||||||
from app.chain import ChainBase
|
from app.chain import ChainBase
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.core.meta import MetaBase
|
from app.core.meta import MetaBase
|
||||||
|
from app.core.metainfo import MetaInfo
|
||||||
|
from app.chain.media import MediaChain
|
||||||
from app.helper.recognize import MediaRecognizeShareHelper
|
from app.helper.recognize import MediaRecognizeShareHelper
|
||||||
from app.schemas.types import MediaType
|
from app.schemas.types import MediaType
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.chain = ChainBase()
|
cls.chain = ChainBase()
|
||||||
|
cls.media_chain = MediaChain()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_meta(name: str, media_type: MediaType = MediaType.UNKNOWN) -> MetaBase:
|
def _build_meta(name: str, media_type: MediaType = MediaType.UNKNOWN) -> MetaBase:
|
||||||
@@ -204,6 +207,114 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
self.assertEqual(query_params["keyword"], "未应用识别词的名称")
|
self.assertEqual(query_params["keyword"], "未应用识别词的名称")
|
||||||
self.assertEqual(report_payload["keyword"], "未应用识别词的名称")
|
self.assertEqual(report_payload["keyword"], "未应用识别词的名称")
|
||||||
|
|
||||||
|
def test_query_and_report_can_use_distinct_keyword_meta(self):
|
||||||
|
"""
|
||||||
|
共享识别应允许用原始关键字上报,同时保留辅助识别后的年份/季信息。
|
||||||
|
"""
|
||||||
|
helper = MediaRecognizeShareHelper()
|
||||||
|
meta = self._build_meta("辅助识别后的名称", MediaType.TV)
|
||||||
|
meta.year = "2024"
|
||||||
|
meta.begin_season = 2
|
||||||
|
|
||||||
|
keyword_meta = self._build_meta("辅助识别前的名称", MediaType.UNKNOWN)
|
||||||
|
keyword_meta.original_name = "辅助识别前的名称"
|
||||||
|
|
||||||
|
mediainfo = MediaInfo(
|
||||||
|
title="测试剧集",
|
||||||
|
year="2024",
|
||||||
|
tmdb_id=401,
|
||||||
|
type=MediaType.TV,
|
||||||
|
season=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
query_params = helper._build_query_params(
|
||||||
|
meta=meta,
|
||||||
|
mtype=None,
|
||||||
|
keyword_meta=keyword_meta,
|
||||||
|
)
|
||||||
|
report_payload = helper._build_report_payload(
|
||||||
|
meta=meta,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
keyword_meta=keyword_meta,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(query_params["keyword"], "辅助识别前的名称")
|
||||||
|
self.assertEqual(query_params["year"], "2024")
|
||||||
|
self.assertEqual(query_params["season"], 2)
|
||||||
|
self.assertEqual(report_payload["keyword"], "辅助识别前的名称")
|
||||||
|
self.assertEqual(report_payload["year"], "2024")
|
||||||
|
self.assertEqual(report_payload["season"], 2)
|
||||||
|
|
||||||
|
def test_report_shared_result_with_distinct_keyword_meta(self):
|
||||||
|
"""
|
||||||
|
辅助识别成功后应按辅助前名称上报共享结果。
|
||||||
|
"""
|
||||||
|
meta = self._build_meta("辅助识别后的名称", MediaType.TV)
|
||||||
|
meta.year = "2024"
|
||||||
|
meta.begin_season = 1
|
||||||
|
share_meta = self._build_meta("辅助识别前的名称", MediaType.UNKNOWN)
|
||||||
|
share_meta.original_name = "辅助识别前的名称"
|
||||||
|
mediainfo = MediaInfo(title="测试剧集", year="2024", tmdb_id=402, type=MediaType.TV)
|
||||||
|
|
||||||
|
with patch.object(self.chain, "run_module", return_value=mediainfo), patch(
|
||||||
|
"app.chain.MediaRecognizeShareHelper.report",
|
||||||
|
return_value=True,
|
||||||
|
) as report_mock:
|
||||||
|
result = self.chain.recognize_media(meta=meta, share_meta=share_meta, cache=False)
|
||||||
|
|
||||||
|
self.assertIs(result, mediainfo)
|
||||||
|
report_mock.assert_called_once_with(
|
||||||
|
meta=meta,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
keyword_meta=share_meta,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_query_shared_result_with_distinct_keyword_meta(self):
|
||||||
|
"""
|
||||||
|
本地识别失败后应按辅助前名称回查共享结果。
|
||||||
|
"""
|
||||||
|
meta = self._build_meta("辅助识别后的名称", MediaType.TV)
|
||||||
|
meta.year = "2024"
|
||||||
|
share_meta = self._build_meta("辅助识别前的名称", MediaType.UNKNOWN)
|
||||||
|
share_meta.original_name = "辅助识别前的名称"
|
||||||
|
shared_media = MediaInfo(title="测试剧集", year="2024", tmdb_id=403, type=MediaType.TV)
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
self.chain,
|
||||||
|
"run_module",
|
||||||
|
side_effect=[None, shared_media],
|
||||||
|
), patch(
|
||||||
|
"app.chain.MediaRecognizeShareHelper.query",
|
||||||
|
return_value={"type": "tv", "tmdbid": 403, "season": 1},
|
||||||
|
) as query_mock, patch(
|
||||||
|
"app.chain.MediaRecognizeShareHelper.to_recognize_params",
|
||||||
|
return_value={
|
||||||
|
"mtype": MediaType.TV,
|
||||||
|
"tmdbid": 403,
|
||||||
|
"doubanid": None,
|
||||||
|
"bangumiid": None,
|
||||||
|
"season": 1,
|
||||||
|
},
|
||||||
|
), patch(
|
||||||
|
"app.chain.MediaRecognizeShareHelper.report",
|
||||||
|
return_value=False,
|
||||||
|
), patch.object(
|
||||||
|
self.chain,
|
||||||
|
"_update_local_recognize_cache",
|
||||||
|
):
|
||||||
|
result = self.chain.recognize_media(
|
||||||
|
meta=meta,
|
||||||
|
share_meta=share_meta,
|
||||||
|
cache=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(result, shared_media)
|
||||||
|
query_mock.assert_called_once_with(
|
||||||
|
meta=meta,
|
||||||
|
mtype=None,
|
||||||
|
keyword_meta=share_meta,
|
||||||
|
)
|
||||||
|
|
||||||
def test_skip_report_when_local_recognize_hits_cache(self):
|
def test_skip_report_when_local_recognize_hits_cache(self):
|
||||||
"""
|
"""
|
||||||
本地识别命中缓存时不应上报共享识别
|
本地识别命中缓存时不应上报共享识别
|
||||||
@@ -255,6 +366,80 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
report_mock.assert_not_awaited()
|
report_mock.assert_not_awaited()
|
||||||
query_mock.assert_not_awaited()
|
query_mock.assert_not_awaited()
|
||||||
|
|
||||||
|
def test_recognize_by_meta_can_skip_obtain_images(self):
|
||||||
|
"""
|
||||||
|
标题识别可显式关闭图片拉取。
|
||||||
|
"""
|
||||||
|
meta = MetaInfo("测试电影")
|
||||||
|
mediainfo = MediaInfo(title="测试电影", year="2024", tmdb_id=404, type=MediaType.MOVIE)
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
self.media_chain,
|
||||||
|
"recognize_media",
|
||||||
|
return_value=mediainfo,
|
||||||
|
) as recognize_mock, patch.object(
|
||||||
|
self.media_chain,
|
||||||
|
"obtain_images",
|
||||||
|
) as obtain_images_mock:
|
||||||
|
result = self.media_chain.recognize_by_meta(meta, obtain_images=False)
|
||||||
|
|
||||||
|
self.assertIs(result, mediainfo)
|
||||||
|
recognize_mock.assert_called_once()
|
||||||
|
obtain_images_mock.assert_not_called()
|
||||||
|
|
||||||
|
def test_recognize_by_meta_reports_with_original_keyword_after_plugin_help(self):
|
||||||
|
"""
|
||||||
|
辅助识别后应继续使用辅助前关键字进行共享上报。
|
||||||
|
"""
|
||||||
|
meta = MetaInfo("辅助前名称")
|
||||||
|
plugin_media = MediaInfo(title="辅助后名称", year="2024", tmdb_id=405, type=MediaType.TV)
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
self.media_chain,
|
||||||
|
"select_recognize_source",
|
||||||
|
side_effect=lambda **kwargs: kwargs["plugin_fn"](),
|
||||||
|
), patch.object(
|
||||||
|
self.media_chain,
|
||||||
|
"recognize_help",
|
||||||
|
return_value=plugin_media,
|
||||||
|
) as recognize_help_mock, patch.object(
|
||||||
|
self.media_chain,
|
||||||
|
"obtain_images",
|
||||||
|
):
|
||||||
|
result = self.media_chain.recognize_by_meta(meta, obtain_images=False)
|
||||||
|
|
||||||
|
self.assertIs(result, plugin_media)
|
||||||
|
self.assertEqual(recognize_help_mock.call_args.kwargs["share_meta"].name, "辅助前名称")
|
||||||
|
|
||||||
|
def test_async_recognize_by_meta_can_skip_obtain_images(self):
|
||||||
|
"""
|
||||||
|
异步标题识别可显式关闭图片拉取。
|
||||||
|
"""
|
||||||
|
meta = MetaInfo("测试异步电影")
|
||||||
|
mediainfo = MediaInfo(title="测试异步电影", year="2025", tmdb_id=406, type=MediaType.MOVIE)
|
||||||
|
|
||||||
|
async def runner():
|
||||||
|
with patch.object(
|
||||||
|
self.media_chain,
|
||||||
|
"async_recognize_media",
|
||||||
|
AsyncMock(return_value=mediainfo),
|
||||||
|
) as recognize_mock, patch.object(
|
||||||
|
self.media_chain,
|
||||||
|
"async_obtain_images",
|
||||||
|
AsyncMock(),
|
||||||
|
) as obtain_images_mock:
|
||||||
|
result = await self.media_chain.async_recognize_by_meta(
|
||||||
|
meta,
|
||||||
|
obtain_images=False,
|
||||||
|
)
|
||||||
|
return result, recognize_mock, obtain_images_mock
|
||||||
|
|
||||||
|
result, recognize_mock, obtain_images_mock = asyncio.run(runner())
|
||||||
|
|
||||||
|
self.assertIs(result, mediainfo)
|
||||||
|
recognize_mock.assert_awaited_once()
|
||||||
|
obtain_images_mock.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user