mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 00:46:57 +08:00
fix: preserve parent year in media recognition
This commit is contained in:
@@ -9,6 +9,7 @@ from app.chain.tmdb import TmdbChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import Context
|
||||
from app.core.event import eventmanager
|
||||
from app.core.meta import MetaBase
|
||||
from app.core.metainfo import MetaInfo, MetaInfoPath
|
||||
from app.core.security import verify_token, verify_apitoken
|
||||
from app.db.models import User
|
||||
@@ -22,6 +23,29 @@ router = APIRouter()
|
||||
MediaSource = str
|
||||
|
||||
|
||||
def _build_recognize_metainfo(
|
||||
title: str,
|
||||
subtitle: Optional[str] = None,
|
||||
custom_words: Optional[str] = None,
|
||||
) -> MetaBase:
|
||||
"""构造标题识别元数据,并兼容第三方客户端传入媒体文件路径。"""
|
||||
custom_word_list = custom_words.split("\n") if custom_words else None
|
||||
normalized_title = title.replace("\\", "/")
|
||||
title_path = Path(normalized_title)
|
||||
if (
|
||||
("/" in title or "\\" in title)
|
||||
and "://" not in title
|
||||
and title_path.suffix.lower() in settings.RMT_MEDIAEXT
|
||||
):
|
||||
metainfo = MetaInfoPath(
|
||||
title_path,
|
||||
custom_words=custom_word_list,
|
||||
)
|
||||
metainfo.title = title
|
||||
return metainfo
|
||||
return MetaInfo(title, subtitle, custom_words=custom_word_list)
|
||||
|
||||
|
||||
def _build_media_seasons(
|
||||
mediainfo: Any, season: Optional[int] = None,
|
||||
) -> List[schemas.MediaSeason]:
|
||||
@@ -84,9 +108,7 @@ async def recognize(
|
||||
:param _:
|
||||
"""
|
||||
# 识别媒体信息,传入临时识别词时优先于系统配置的识别词生效
|
||||
metainfo = MetaInfo(
|
||||
title, subtitle, custom_words=custom_words.split("\n") if custom_words else None
|
||||
)
|
||||
metainfo = _build_recognize_metainfo(title, subtitle, custom_words)
|
||||
mediainfo = await MediaChain().async_recognize_by_meta(
|
||||
metainfo,
|
||||
source=source,
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ FastAPI 异常响应保留 `detail` 字段,并在错误详情为文本时返
|
||||
| 方法 | 路径 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| GET | `/api/v1/media/search` | 按标题搜索媒体、合集或人物,参数:`title`、`type`、`page`、`count`,可选 `source`;`media` 支持 `themoviedb`、`douban`、`bangumi`、`anilist`,`collection` 支持 `themoviedb`,`person` 支持 `themoviedb`、`douban` |
|
||||
| GET | `/api/v1/media/recognize` | 识别标题,参数:`title`、`subtitle`、`custom_words`,可选 `source` |
|
||||
| GET | `/api/v1/media/recognize` | 识别标题,参数:`title`、`subtitle`、`custom_words`,可选 `source`;当 `title` 为含目录的媒体文件路径时,会合并父目录中的名称、年份等信息 |
|
||||
| GET | `/api/v1/media/recognize_file` | 识别文件路径,参数:`path`,可选 `source` |
|
||||
| GET | `/api/v1/media/{mediaid}` | 查询媒体详情,`mediaid` 支持 `tmdb:`、`douban:`、`bangumi:`、`anilist:` 及插件自定义来源前缀 |
|
||||
| POST | `/api/v1/media/scrape/{storage}` | 刮削媒体元数据;请求体为 `FileItem`,可选查询参数 `media_source`、`media_id`、`type_name`(电影/电视剧)可指定本次刮削媒体 |
|
||||
|
||||
@@ -92,8 +92,8 @@ All endpoints are under the base URL `{MP_HOST}`. Path parameters are shown as `
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/media/search` | Search media, collections, or people by title. Params: `title` (required), `type`, `page`, `count`, optional `source`. Supported sources: `media` = `themoviedb`, `douban`, `bangumi`, `anilist`; `collection` = `themoviedb`; `person` = `themoviedb`, `douban` |
|
||||
| GET | `/api/v1/media/recognize` | Recognize media from torrent title. Params: `title` (required), `subtitle`, `custom_words`, optional `source` |
|
||||
| GET | `/api/v1/media/recognize2` | Recognize media (API_TOKEN auth, use `--token-param`). Params: `title`, `subtitle`, `custom_words`, optional `source` |
|
||||
| GET | `/api/v1/media/recognize` | Recognize media from a torrent title or a media file path. Params: `title` (required), `subtitle`, `custom_words`, optional `source`; media file paths also use parent-directory metadata such as title and year |
|
||||
| GET | `/api/v1/media/recognize2` | Recognize media from a torrent title or media file path (API_TOKEN auth, use `--token-param`). Params: `title`, `subtitle`, `custom_words`, optional `source`; media file paths also use parent-directory metadata |
|
||||
| GET | `/api/v1/media/recognize_file` | Recognize media from file path. Params: `path` (required), optional `source` |
|
||||
| GET | `/api/v1/media/recognize_file2` | Recognize file (API_TOKEN auth). Params: `path`, optional `source` |
|
||||
| POST | `/api/v1/media/scrape/{storage}` | Scrape media metadata. Body: FileItem JSON. Optional params: `media_source`, `media_id`, `type_name` (`电影`/`电视剧`) |
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.api.endpoints.media import recognize
|
||||
from app.core.context import MediaInfo
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"title",
|
||||
[
|
||||
"武神主宰 (2020)/武神主宰.S01E680.mp4",
|
||||
r"D:\武神主宰 (2020)\武神主宰.S01E680.mp4",
|
||||
],
|
||||
)
|
||||
def test_recognize_uses_parent_metadata_for_media_file_path(title: str) -> None:
|
||||
"""标题参数为媒体文件路径时应合并父目录中的名称和年份。"""
|
||||
chain = Mock()
|
||||
chain.async_recognize_by_meta = AsyncMock(
|
||||
return_value=MediaInfo(
|
||||
title="武神主宰",
|
||||
year="2020",
|
||||
type=MediaType.TV,
|
||||
)
|
||||
)
|
||||
|
||||
with patch("app.api.endpoints.media.MediaChain", return_value=chain):
|
||||
asyncio.run(recognize(title=title, _=Mock()))
|
||||
|
||||
metainfo = chain.async_recognize_by_meta.await_args.args[0]
|
||||
assert metainfo.name == "武神主宰"
|
||||
assert metainfo.year == "2020"
|
||||
assert metainfo.begin_season == 1
|
||||
assert metainfo.begin_episode == 680
|
||||
assert metainfo.title == title
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"title",
|
||||
[
|
||||
"Fate/stay night",
|
||||
"https://example.com/武神主宰.S01E680.mp4",
|
||||
],
|
||||
)
|
||||
def test_recognize_does_not_treat_non_path_title_as_file_path(title: str) -> None:
|
||||
"""普通片名和网络地址不应误走文件路径解析。"""
|
||||
chain = Mock()
|
||||
chain.async_recognize_by_meta = AsyncMock(
|
||||
return_value=MediaInfo(title="Fate/stay night", type=MediaType.TV)
|
||||
)
|
||||
|
||||
with (
|
||||
patch("app.api.endpoints.media.MediaChain", return_value=chain),
|
||||
patch("app.api.endpoints.media.MetaInfoPath") as meta_info_path,
|
||||
):
|
||||
asyncio.run(recognize(title=title, _=Mock()))
|
||||
|
||||
meta_info_path.assert_not_called()
|
||||
Reference in New Issue
Block a user