fix: preserve parent year in media recognition

This commit is contained in:
jxxghp
2026-08-06 12:45:48 +08:00
parent 7fe7be6d71
commit 4abae809c2
4 changed files with 88 additions and 6 deletions
+25 -3
View File
@@ -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
View File
@@ -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`(电影/电视剧)可指定本次刮削媒体 |
+2 -2
View File
@@ -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` (`电影`/`电视剧`) |
+60
View File
@@ -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()