mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +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.config import settings
|
||||||
from app.core.context import Context
|
from app.core.context import Context
|
||||||
from app.core.event import eventmanager
|
from app.core.event import eventmanager
|
||||||
|
from app.core.meta import MetaBase
|
||||||
from app.core.metainfo import MetaInfo, MetaInfoPath
|
from app.core.metainfo import MetaInfo, MetaInfoPath
|
||||||
from app.core.security import verify_token, verify_apitoken
|
from app.core.security import verify_token, verify_apitoken
|
||||||
from app.db.models import User
|
from app.db.models import User
|
||||||
@@ -22,6 +23,29 @@ router = APIRouter()
|
|||||||
MediaSource = str
|
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(
|
def _build_media_seasons(
|
||||||
mediainfo: Any, season: Optional[int] = None,
|
mediainfo: Any, season: Optional[int] = None,
|
||||||
) -> List[schemas.MediaSeason]:
|
) -> List[schemas.MediaSeason]:
|
||||||
@@ -84,9 +108,7 @@ async def recognize(
|
|||||||
:param _:
|
:param _:
|
||||||
"""
|
"""
|
||||||
# 识别媒体信息,传入临时识别词时优先于系统配置的识别词生效
|
# 识别媒体信息,传入临时识别词时优先于系统配置的识别词生效
|
||||||
metainfo = MetaInfo(
|
metainfo = _build_recognize_metainfo(title, subtitle, custom_words)
|
||||||
title, subtitle, custom_words=custom_words.split("\n") if custom_words else None
|
|
||||||
)
|
|
||||||
mediainfo = await MediaChain().async_recognize_by_meta(
|
mediainfo = await MediaChain().async_recognize_by_meta(
|
||||||
metainfo,
|
metainfo,
|
||||||
source=source,
|
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/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/recognize_file` | 识别文件路径,参数:`path`,可选 `source` |
|
||||||
| GET | `/api/v1/media/{mediaid}` | 查询媒体详情,`mediaid` 支持 `tmdb:`、`douban:`、`bangumi:`、`anilist:` 及插件自定义来源前缀 |
|
| GET | `/api/v1/media/{mediaid}` | 查询媒体详情,`mediaid` 支持 `tmdb:`、`douban:`、`bangumi:`、`anilist:` 及插件自定义来源前缀 |
|
||||||
| POST | `/api/v1/media/scrape/{storage}` | 刮削媒体元数据;请求体为 `FileItem`,可选查询参数 `media_source`、`media_id`、`type_name`(电影/电视剧)可指定本次刮削媒体 |
|
| 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 |
|
| 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/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/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 (API_TOKEN auth, use `--token-param`). Params: `title`, `subtitle`, `custom_words`, optional `source` |
|
| 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_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` |
|
| 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` (`电影`/`电视剧`) |
|
| 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