Codex/fix season subscription poster (#6185)

This commit is contained in:
jxxghp
2026-07-25 08:07:08 +08:00
committed by GitHub
parent 6354a48405
commit ff9ba79b60
12 changed files with 439 additions and 38 deletions

View File

@@ -61,6 +61,7 @@ class DeleteSubscribeTool(MoviePilotTool):
"anilistid": subscribe.anilistid,
"media_source": subscribe.media_source,
"media_id": subscribe.media_id,
"season": subscribe.season,
}
)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
# 类型