mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
Codex/fix season subscription poster (#6185)
This commit is contained in:
@@ -61,6 +61,7 @@ class DeleteSubscribeTool(MoviePilotTool):
|
||||
"anilistid": subscribe.anilistid,
|
||||
"media_source": subscribe.media_source,
|
||||
"media_id": subscribe.media_id,
|
||||
"season": subscribe.season,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -863,6 +863,7 @@ async def delete_subscribe(
|
||||
"anilistid": subscribe_info.get("anilistid"),
|
||||
"media_source": subscribe_info.get("media_source"),
|
||||
"media_id": subscribe_info.get("media_id"),
|
||||
"season": subscribe_info.get("season"),
|
||||
}
|
||||
)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@@ -11,6 +11,7 @@ from urllib.parse import parse_qs, urljoin, urlparse
|
||||
|
||||
from app import schemas
|
||||
from app.chain import ChainBase
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.storage import StorageChain
|
||||
from app.core.cache import FileCache
|
||||
from app.core.config import settings, global_vars
|
||||
@@ -402,6 +403,7 @@ class DownloadChain(ChainBase):
|
||||
)
|
||||
if not mediainfo:
|
||||
return False, "无法识别媒体信息", []
|
||||
mediainfo = MediaChain().supplement_tmdb_info(mediainfo, metainfo)
|
||||
|
||||
storage, target_dir, error_msg = self._resolve_media_download_dir(
|
||||
media_info=mediainfo,
|
||||
@@ -804,6 +806,10 @@ class DownloadChain(ChainBase):
|
||||
_meta = context.meta_info
|
||||
_site_downloader = _torrent.site_downloader
|
||||
|
||||
# 下载目录和下载器分类依赖 TMDB 辅助分类,但媒体主身份保持不变。
|
||||
_media = MediaChain().supplement_tmdb_info(_media, _meta)
|
||||
context.media_info = _media
|
||||
|
||||
# 发送资源下载事件,允许外部拦截下载
|
||||
event_data = ResourceDownloadEventData(
|
||||
context=context,
|
||||
@@ -839,15 +845,6 @@ class DownloadChain(ChainBase):
|
||||
logger.warn(str(err))
|
||||
return (None, str(err)) if return_detail else None
|
||||
|
||||
# 补充完整的media数据
|
||||
if not _media.genre_ids:
|
||||
new_media = self.recognize_media(mtype=_media.type, tmdbid=_media.tmdb_id,
|
||||
doubanid=_media.douban_id, bangumiid=_media.bangumi_id,
|
||||
anilistid=_media.anilist_id,
|
||||
episode_group=_media.episode_group)
|
||||
if new_media:
|
||||
_media = new_media
|
||||
|
||||
# 实际下载的集数
|
||||
download_episodes = StringUtils.format_ep(list(episodes)) if episodes else None
|
||||
if episodes is not None:
|
||||
|
||||
@@ -614,6 +614,113 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
logger.warn(f"{metainfo.title} 未识别到媒体信息")
|
||||
return mediainfo
|
||||
|
||||
@staticmethod
|
||||
def _build_tmdb_supplement_meta(
|
||||
mediainfo: MediaInfo,
|
||||
metainfo: Optional[MetaBase] = None,
|
||||
) -> MetaBase:
|
||||
"""
|
||||
根据主识别结果构造 TMDB 辅助识别参数。
|
||||
|
||||
:param mediainfo: 主识别源返回的媒体信息
|
||||
:param metainfo: 原始标题解析信息
|
||||
:return: 不携带主识别源身份的 TMDB 查询参数
|
||||
"""
|
||||
title = mediainfo.title or getattr(metainfo, "name", None) or ""
|
||||
tmdb_meta = MetaInfo(title)
|
||||
if not tmdb_meta.cn_name and getattr(metainfo, "cn_name", None):
|
||||
tmdb_meta.cn_name = metainfo.cn_name
|
||||
if not tmdb_meta.en_name:
|
||||
tmdb_meta.en_name = mediainfo.en_title or (
|
||||
getattr(metainfo, "en_name", None)
|
||||
)
|
||||
tmdb_meta.type = mediainfo.type or (
|
||||
getattr(metainfo, "type", None) or MediaType.UNKNOWN
|
||||
)
|
||||
season = (
|
||||
mediainfo.season
|
||||
if mediainfo.season is not None
|
||||
else getattr(metainfo, "begin_season", None)
|
||||
)
|
||||
tmdb_meta.begin_season = season
|
||||
season_year = None
|
||||
if season is not None and mediainfo.season_years:
|
||||
season_year = (
|
||||
mediainfo.season_years.get(season)
|
||||
or mediainfo.season_years.get(str(season))
|
||||
)
|
||||
tmdb_meta.year = (
|
||||
season_year
|
||||
or mediainfo.year
|
||||
or getattr(metainfo, "year", None)
|
||||
)
|
||||
return tmdb_meta
|
||||
|
||||
@staticmethod
|
||||
def _merge_tmdb_auxiliary(
|
||||
mediainfo: MediaInfo,
|
||||
tmdb_media: MediaInfo,
|
||||
) -> MediaInfo:
|
||||
"""
|
||||
将 TMDB 兼容字段合并到主识别结果,不改变主数据源身份和展示信息。
|
||||
|
||||
:param mediainfo: 主识别源返回的媒体信息
|
||||
:param tmdb_media: TMDB 辅助识别结果
|
||||
:return: 已补充 TMDB 兼容字段的主媒体信息
|
||||
"""
|
||||
if not tmdb_media or tmdb_media.source != "themoviedb" or not tmdb_media.tmdb_id:
|
||||
return mediainfo
|
||||
|
||||
mediainfo.tmdb_id = tmdb_media.tmdb_id
|
||||
mediainfo.tmdb_info = tmdb_media.tmdb_info or mediainfo.tmdb_info
|
||||
if not mediainfo.category:
|
||||
mediainfo.category = tmdb_media.category
|
||||
if not mediainfo.genre_ids:
|
||||
mediainfo.genre_ids = list(tmdb_media.genre_ids or [])
|
||||
for field in ("imdb_id", "tvdb_id", "collection_id"):
|
||||
if not getattr(mediainfo, field, None):
|
||||
setattr(mediainfo, field, getattr(tmdb_media, field, None))
|
||||
return mediainfo
|
||||
|
||||
def supplement_tmdb_info(
|
||||
self,
|
||||
mediainfo: Optional[MediaInfo],
|
||||
metainfo: Optional[MetaBase] = None,
|
||||
) -> Optional[MediaInfo]:
|
||||
"""
|
||||
为任意主识别源补充 TMDB 辅助信息,同时保留原始媒体身份。
|
||||
|
||||
:param mediainfo: 主识别源返回的媒体信息
|
||||
:param metainfo: 原始标题解析信息
|
||||
:return: 已补充 TMDB 辅助字段的原媒体对象
|
||||
"""
|
||||
if not mediainfo:
|
||||
return None
|
||||
if mediainfo.tmdb_id and mediainfo.tmdb_info and mediainfo.genre_ids:
|
||||
return mediainfo
|
||||
tmdb_meta = self._build_tmdb_supplement_meta(mediainfo, metainfo)
|
||||
tmdb_module = self.modulemanager.get_running_module("TheMovieDbModule")
|
||||
if not tmdb_module:
|
||||
logger.warn("TMDB 模块未启用,无法补充 TMDB 辅助信息")
|
||||
return mediainfo
|
||||
try:
|
||||
tmdb_media = tmdb_module.recognize_media(
|
||||
meta=tmdb_meta,
|
||||
mtype=mediainfo.type,
|
||||
source="themoviedb",
|
||||
mediaid=str(mediainfo.tmdb_id) if mediainfo.tmdb_id else None,
|
||||
tmdbid=mediainfo.tmdb_id,
|
||||
episode_group=mediainfo.episode_group,
|
||||
cache=True,
|
||||
)
|
||||
except Exception as err:
|
||||
logger.warn(f"{mediainfo.title_year} 补充 TMDB 辅助信息失败:{err}")
|
||||
return mediainfo
|
||||
if not tmdb_media:
|
||||
logger.warn(f"{mediainfo.title_year} 未匹配到 TMDB 辅助信息")
|
||||
return mediainfo
|
||||
return self._merge_tmdb_auxiliary(mediainfo, tmdb_media)
|
||||
|
||||
def _recognize_with_fallback_by_meta(
|
||||
self,
|
||||
metainfo: MetaBase,
|
||||
|
||||
+19
-4
@@ -1055,7 +1055,7 @@ class SubscribeChain(ChainBase):
|
||||
"anilistid": mediainfo.anilist_id,
|
||||
"media_source": media_source,
|
||||
"media_id": media_id,
|
||||
"season": metainfo.begin_season,
|
||||
"season": season,
|
||||
"poster": mediainfo.get_poster_image(),
|
||||
"backdrop": mediainfo.get_backdrop_image(),
|
||||
"vote": mediainfo.vote_average,
|
||||
@@ -1250,7 +1250,7 @@ class SubscribeChain(ChainBase):
|
||||
"anilistid": mediainfo.anilist_id,
|
||||
"media_source": media_source,
|
||||
"media_id": media_id,
|
||||
"season": metainfo.begin_season,
|
||||
"season": season,
|
||||
"poster": mediainfo.get_poster_image(),
|
||||
"backdrop": mediainfo.get_backdrop_image(),
|
||||
"vote": mediainfo.vote_average,
|
||||
@@ -2835,7 +2835,12 @@ class SubscribeChain(ChainBase):
|
||||
# 统计订阅
|
||||
MoviePilotServerHelper.sub_done_async({
|
||||
"tmdbid": mediainfo.tmdb_id,
|
||||
"doubanid": mediainfo.douban_id
|
||||
"doubanid": mediainfo.douban_id,
|
||||
"bangumiid": mediainfo.bangumi_id,
|
||||
"anilistid": mediainfo.anilist_id,
|
||||
"media_source": subscribe.media_source,
|
||||
"media_id": subscribe.media_id,
|
||||
"season": subscribe.season,
|
||||
})
|
||||
|
||||
def remote_list(
|
||||
@@ -3490,6 +3495,11 @@ class SubscribeChain(ChainBase):
|
||||
{
|
||||
"tmdbid": subscribe.tmdbid,
|
||||
"doubanid": subscribe.doubanid,
|
||||
"bangumiid": subscribe.bangumiid,
|
||||
"anilistid": subscribe.anilistid,
|
||||
"media_source": subscribe.media_source,
|
||||
"media_id": subscribe.media_id,
|
||||
"season": subscribe.season,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3537,7 +3547,12 @@ class SubscribeChain(ChainBase):
|
||||
# 统计订阅
|
||||
MoviePilotServerHelper.sub_done_async({
|
||||
"tmdbid": subscribe.tmdbid,
|
||||
"doubanid": subscribe.doubanid
|
||||
"doubanid": subscribe.doubanid,
|
||||
"bangumiid": subscribe.bangumiid,
|
||||
"anilistid": subscribe.anilistid,
|
||||
"media_source": subscribe.media_source,
|
||||
"media_id": subscribe.media_id,
|
||||
"season": subscribe.season,
|
||||
})
|
||||
# 重新发送消息
|
||||
self.remote_list(channel=channel, userid=userid, source=source)
|
||||
|
||||
+52
-17
@@ -55,7 +55,7 @@ from app.schemas.types import (
|
||||
ContentType,
|
||||
)
|
||||
from app.utils.mixins import ConfigReloadMixin
|
||||
from app.utils.media import parse_media_key
|
||||
from app.utils.media import normalize_media_source, parse_media_key, resolve_media_identity
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.system import SystemUtils
|
||||
@@ -142,19 +142,8 @@ class JobManager:
|
||||
"""
|
||||
if not media:
|
||||
return None, season
|
||||
media_ids = {
|
||||
"themoviedb": media.tmdb_id,
|
||||
"douban": media.douban_id,
|
||||
"bangumi": media.bangumi_id,
|
||||
"anilist": media.anilist_id,
|
||||
}
|
||||
source = media.source
|
||||
if not source or media_ids.get(source) is None:
|
||||
source = next(
|
||||
(name for name, media_id in media_ids.items() if media_id is not None),
|
||||
source,
|
||||
)
|
||||
return (source, media_ids.get(source)), season
|
||||
source, media_id = resolve_media_identity(media=media)
|
||||
return (source, media_id), season
|
||||
|
||||
@staticmethod
|
||||
def __get_file_key(fileitem: FileItem) -> Optional[Tuple[str, str]]:
|
||||
@@ -794,6 +783,23 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
"TRANSFER_THREADS",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _requires_automatic_category(task: TransferTask) -> bool:
|
||||
"""
|
||||
判断当前整理任务是否需要根据媒体识别结果自动创建类别目录。
|
||||
|
||||
:param task: 整理任务
|
||||
:return: 是否必须具备自动分类结果
|
||||
"""
|
||||
target_directory = task.target_directory
|
||||
if target_directory and target_directory.media_category:
|
||||
return False
|
||||
if task.library_category_folder is not None:
|
||||
return bool(task.library_category_folder)
|
||||
return bool(
|
||||
target_directory and target_directory.library_category_folder
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
"""初始化文件整理处理链。"""
|
||||
super().__init__()
|
||||
@@ -1709,8 +1715,15 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
|
||||
mediainfo_changed = True
|
||||
|
||||
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
|
||||
if not settings.SCRAP_FOLLOW_TMDB:
|
||||
# TMDB 仅作为辅助信息合并,不能改变原识别源的主身份和标题。
|
||||
mediainfo = MediaChain().supplement_tmdb_info(mediainfo, task.meta)
|
||||
task.mediainfo = mediainfo
|
||||
|
||||
# 只有 TMDB 主源沿用历史 TMDB 标题,避免辅助 ID 改写其它识别源标题。
|
||||
if (
|
||||
not settings.SCRAP_FOLLOW_TMDB
|
||||
and normalize_media_source(mediainfo.source) == "themoviedb"
|
||||
):
|
||||
transfer_history = transferhis.get_by_type_tmdbid(
|
||||
tmdbid=mediainfo.tmdb_id, mtype=mediainfo.type.value
|
||||
)
|
||||
@@ -1727,7 +1740,11 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
return False, f"{task.fileitem.name} 已在整理队列中"
|
||||
|
||||
# 获取集数据
|
||||
if task.mediainfo.type == MediaType.TV and not task.episodes_info:
|
||||
if (
|
||||
task.mediainfo.type == MediaType.TV
|
||||
and task.mediainfo.tmdb_id
|
||||
and not task.episodes_info
|
||||
):
|
||||
# 判断注意season为0的情况
|
||||
season_num = task.mediainfo.season
|
||||
if season_num is None and task.meta.season_seq:
|
||||
@@ -1762,6 +1779,24 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
if not task.target_storage and task.target_directory:
|
||||
task.target_storage = task.target_directory.library_storage
|
||||
|
||||
if self._requires_automatic_category(task) and not task.mediainfo.category:
|
||||
if task.mediainfo.tmdb_id:
|
||||
error_message = "TMDB 信息未匹配到媒体分类,无法按媒体类别整理"
|
||||
else:
|
||||
error_message = "未识别到 TMDB 辅助信息,无法按媒体类别整理"
|
||||
logger.error(f"{task.fileitem.name} {error_message}")
|
||||
if callback:
|
||||
return callback(
|
||||
task,
|
||||
TransferInfo(
|
||||
success=False,
|
||||
fileitem=task.fileitem,
|
||||
transfer_type=task.transfer_type,
|
||||
message=error_message,
|
||||
),
|
||||
)
|
||||
return False, error_message
|
||||
|
||||
# 正在处理
|
||||
self.jobview.running_task(task)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, List, Literal, Optional
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -60,9 +60,7 @@ class TransferTask(BaseModel):
|
||||
fileitem: FileItem
|
||||
meta: Optional[Any] = None
|
||||
mediainfo: Optional[Any] = None
|
||||
media_source: Optional[
|
||||
Literal["themoviedb", "douban", "bangumi", "anilist"]
|
||||
] = None
|
||||
media_source: Optional[str] = None
|
||||
target_directory: Optional[TransferDirectoryConf] = None
|
||||
target_storage: Optional[str] = None
|
||||
target_path: Optional[Path] = None
|
||||
@@ -220,9 +218,7 @@ class ManualTransferItem(BaseModel):
|
||||
# AniList ID
|
||||
anilistid: Optional[int] = None
|
||||
# 媒体数据源
|
||||
media_source: Optional[
|
||||
Literal["themoviedb", "douban", "bangumi", "anilist"]
|
||||
] = None
|
||||
media_source: Optional[str] = None
|
||||
# 数据源原生ID
|
||||
media_id: Optional[str] = None
|
||||
# 类型
|
||||
|
||||
@@ -2,6 +2,8 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import app.chain.download as download_module
|
||||
from app.chain.download import DownloadChain
|
||||
from app.core.config import settings
|
||||
@@ -11,6 +13,21 @@ from app.schemas import FileItem, NotExistMediaInfo, TransferDirectoryConf
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_tmdb_supplement(monkeypatch):
|
||||
"""隔离下载用例中的 TMDB 辅助识别外部边界。"""
|
||||
|
||||
class _NoopMediaChain:
|
||||
"""保持原媒体对象不变的 TMDB 辅助识别替身。"""
|
||||
|
||||
@staticmethod
|
||||
def supplement_tmdb_info(media, _meta):
|
||||
"""返回原媒体对象。"""
|
||||
return media
|
||||
|
||||
monkeypatch.setattr(download_module, "MediaChain", _NoopMediaChain)
|
||||
|
||||
|
||||
class _FakeDownloadHistoryOper:
|
||||
"""
|
||||
避免单元测试写入真实下载历史,只验证下载链路的控制流。
|
||||
@@ -176,6 +193,54 @@ def test_download_single_submits_download_added_to_background(monkeypatch):
|
||||
)
|
||||
|
||||
|
||||
def test_download_single_supplements_category_before_download_event(monkeypatch):
|
||||
"""下载事件和目录选择前应已有 TMDB 分类,同时保留原识别源身份。"""
|
||||
captured = {}
|
||||
|
||||
class _FakeMediaChain:
|
||||
"""模拟 TMDB 辅助识别并记录调用。"""
|
||||
|
||||
@staticmethod
|
||||
def supplement_tmdb_info(media, _meta):
|
||||
"""给原媒体对象补充下载分类。"""
|
||||
media.tmdb_id = 12345
|
||||
media.category = "日本动画"
|
||||
return media
|
||||
|
||||
def cancel_download(_event_type, event_data):
|
||||
"""捕获下载事件后取消,避免进入真实下载流程。"""
|
||||
captured["event_data"] = event_data
|
||||
event_data.cancel = True
|
||||
return SimpleNamespace(event_data=event_data)
|
||||
|
||||
monkeypatch.setattr(download_module, "MediaChain", _FakeMediaChain)
|
||||
monkeypatch.setattr(download_module.eventmanager, "send_event", cancel_download)
|
||||
media = MediaInfo(
|
||||
source="bangumi",
|
||||
media_id="40000",
|
||||
bangumi_id=40000,
|
||||
type=MediaType.TV,
|
||||
title="测试动画",
|
||||
)
|
||||
context = Context(
|
||||
meta_info=MetaInfo("测试动画 S01"),
|
||||
media_info=media,
|
||||
torrent_info=TorrentInfo(title="测试动画 S01"),
|
||||
)
|
||||
|
||||
result = DownloadChain.__new__(DownloadChain).download_single(
|
||||
context=context,
|
||||
torrent_content="magnet:?xt=urn:btih:test",
|
||||
return_detail=True,
|
||||
)
|
||||
|
||||
assert result == (None, "下载被事件取消")
|
||||
assert captured["event_data"].options["media_category"] == "日本动画"
|
||||
assert context.media_info.source == "bangumi"
|
||||
assert context.media_info.media_id == "40000"
|
||||
assert context.media_info.tmdb_id == 12345
|
||||
|
||||
|
||||
def test_download_single_persists_custom_words_snapshot(monkeypatch):
|
||||
"""下载成功登记历史时,应把传入的订阅识别词原样存入快照,供整理时原样复现识别。"""
|
||||
captured = {}
|
||||
|
||||
@@ -18,6 +18,21 @@ from app.schemas import DownloaderTorrent, TransferDirectoryConf
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_tmdb_supplement(monkeypatch):
|
||||
"""隔离下载路径用例中的 TMDB 辅助识别外部边界。"""
|
||||
|
||||
class _NoopMediaChain:
|
||||
"""保持原媒体对象不变的 TMDB 辅助识别替身。"""
|
||||
|
||||
@staticmethod
|
||||
def supplement_tmdb_info(media, _meta):
|
||||
"""返回原媒体对象。"""
|
||||
return media
|
||||
|
||||
monkeypatch.setattr(download_module, "MediaChain", _NoopMediaChain)
|
||||
|
||||
|
||||
def _download_dirs():
|
||||
return [
|
||||
TransferDirectoryConf(
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.chain.media import MediaChain
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
class _FakeTmdbModule:
|
||||
"""返回固定 TMDB 结果,避免测试访问外部元数据服务。"""
|
||||
|
||||
def __init__(self, result: MediaInfo):
|
||||
"""保存测试需要返回的 TMDB 媒体信息。"""
|
||||
self.result = result
|
||||
|
||||
def recognize_media(self, **_kwargs):
|
||||
"""同步返回固定 TMDB 媒体信息。"""
|
||||
return self.result
|
||||
|
||||
|
||||
def _make_chain(tmdb_media: MediaInfo) -> MediaChain:
|
||||
"""构造不加载真实模块的媒体处理链。"""
|
||||
chain = object.__new__(MediaChain)
|
||||
module = _FakeTmdbModule(tmdb_media)
|
||||
chain.modulemanager = SimpleNamespace(
|
||||
get_running_module=lambda module_id: (
|
||||
module if module_id == "TheMovieDbModule" else None
|
||||
)
|
||||
)
|
||||
return chain
|
||||
|
||||
|
||||
def test_supplement_tmdb_keeps_custom_source_identity() -> None:
|
||||
"""自定义识别源补充 TMDB 后,主身份和展示字段必须保持不变。"""
|
||||
primary = MediaInfo(
|
||||
source="plugin-anime",
|
||||
media_id="subject-42",
|
||||
type=MediaType.TV,
|
||||
title="原识别标题",
|
||||
year="2024",
|
||||
category="",
|
||||
)
|
||||
tmdb_media = MediaInfo(
|
||||
tmdb_info={
|
||||
"id": 12345,
|
||||
"media_type": MediaType.TV,
|
||||
"name": "TMDB 标题",
|
||||
"genre_ids": [16, 18],
|
||||
"external_ids": {"imdb_id": "tt12345", "tvdb_id": 6789},
|
||||
}
|
||||
)
|
||||
tmdb_media.category = "日本动画"
|
||||
|
||||
result = _make_chain(tmdb_media).supplement_tmdb_info(
|
||||
primary, MetaInfo("原识别标题 2024")
|
||||
)
|
||||
|
||||
assert result is primary
|
||||
assert result.source == "plugin-anime"
|
||||
assert result.media_id == "subject-42"
|
||||
assert result.title == "原识别标题"
|
||||
assert result.tmdb_id == 12345
|
||||
assert result.genre_ids == [16, 18]
|
||||
assert result.category == "日本动画"
|
||||
|
||||
|
||||
def test_supplement_tmdb_does_not_override_custom_category() -> None:
|
||||
"""下载历史或目录指定的自定义分类优先于 TMDB 自动分类。"""
|
||||
primary = MediaInfo(
|
||||
source="douban",
|
||||
media_id="35593344",
|
||||
douban_id="35593344",
|
||||
type=MediaType.MOVIE,
|
||||
title="测试电影",
|
||||
category="纪录片",
|
||||
)
|
||||
tmdb_media = MediaInfo(
|
||||
tmdb_info={
|
||||
"id": 9876,
|
||||
"media_type": MediaType.MOVIE,
|
||||
"title": "Test Movie",
|
||||
"genre_ids": [28],
|
||||
}
|
||||
)
|
||||
tmdb_media.category = "动作片"
|
||||
|
||||
result = _make_chain(tmdb_media).supplement_tmdb_info(primary)
|
||||
|
||||
assert result.source == "douban"
|
||||
assert result.media_id == "35593344"
|
||||
assert result.category == "纪录片"
|
||||
assert result.tmdb_id == 9876
|
||||
|
||||
|
||||
def test_tmdb_supplement_uses_current_season_year_and_keeps_season_zero() -> None:
|
||||
"""电视剧优先使用当前季年份,并且特别季季号不能退化为空。"""
|
||||
media = MediaInfo(
|
||||
source="bangumi",
|
||||
media_id="42",
|
||||
type=MediaType.TV,
|
||||
title="测试动画",
|
||||
year="2020",
|
||||
season=0,
|
||||
season_years={0: "2024"},
|
||||
)
|
||||
|
||||
tmdb_meta = MediaChain._build_tmdb_supplement_meta(
|
||||
media, MetaInfo("测试动画 S00 2020")
|
||||
)
|
||||
|
||||
assert tmdb_meta.begin_season == 0
|
||||
assert tmdb_meta.year == "2024"
|
||||
@@ -98,7 +98,8 @@ def test_conflicting_download_history_recognizes_movie_by_file_meta(monkeypatch)
|
||||
lambda: SimpleNamespace(
|
||||
recognize_by_meta=lambda meta, obtain_images: (
|
||||
recognized_meta.append(meta) or fallback_media
|
||||
)
|
||||
),
|
||||
supplement_tmdb_info=lambda media, _meta: media,
|
||||
),
|
||||
)
|
||||
task = TransferTask(
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.schemas import FileItem, TransferDirectoryConf, TransferTask
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
def test_transfer_stops_when_automatic_category_has_no_tmdb_result(monkeypatch) -> None:
|
||||
"""启用自动类别目录时,缺少 TMDB 分类必须在文件操作前明确失败。"""
|
||||
chain = object.__new__(TransferChain)
|
||||
chain.jobview = SimpleNamespace(try_remove_job=lambda _task: None)
|
||||
monkeypatch.setattr(
|
||||
"app.chain.transfer.TransferHistoryOper",
|
||||
lambda: SimpleNamespace(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.chain.transfer.MediaChain",
|
||||
lambda: SimpleNamespace(
|
||||
supplement_tmdb_info=lambda media, _meta: media,
|
||||
),
|
||||
)
|
||||
task = TransferTask(
|
||||
fileitem=FileItem(
|
||||
storage="local",
|
||||
path="/downloads/Test.Movie.2024.mkv",
|
||||
type="file",
|
||||
name="Test.Movie.2024.mkv",
|
||||
extension="mkv",
|
||||
size=1024,
|
||||
),
|
||||
meta=MetaInfo("Test Movie 2024"),
|
||||
mediainfo=MediaInfo(
|
||||
source="anilist",
|
||||
media_id="1234",
|
||||
anilist_id=1234,
|
||||
type=MediaType.MOVIE,
|
||||
title="Test Movie",
|
||||
year="2024",
|
||||
),
|
||||
target_directory=TransferDirectoryConf(
|
||||
library_storage="local",
|
||||
library_path="/library",
|
||||
library_category_folder=True,
|
||||
),
|
||||
library_category_folder=True,
|
||||
preview=True,
|
||||
)
|
||||
|
||||
state, message = chain._TransferChain__handle_transfer(task)
|
||||
|
||||
assert not state
|
||||
assert message == "未识别到 TMDB 辅助信息,无法按媒体类别整理"
|
||||
assert task.mediainfo.source == "anilist"
|
||||
assert task.mediainfo.media_id == "1234"
|
||||
Reference in New Issue
Block a user