Files
MoviePilot/app/modules/themoviedb/__init__.py
T
jxxghp cdab54254d fix(architecture): satisfy ruff/mypy ratchets for media auxiliary
e02418bac 把新导入放在 settings = RuntimeSettingsCompat() 之后,
导致 5 个模块 E402 诊断超出只降不增基线;同时 media_auxiliary
把工厂函数 MetaInfo 误用作返回类型标注。将新增导入移回赋值语句
之前,返回类型修正为 MetaBase。
2026-08-26 14:33:36 +08:00

1592 lines
64 KiB
Python

import re
from typing import Dict, List, Optional, Tuple, Union
import cn2an
from app.modules._base.media_auxiliary import MediaAuxiliaryProviderMixin
from app.runtime.settings import get_runtime_setting
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.schemas.tmdb import TmdbEpisode as _SchemaTmdbEpisode
from app.schemas.tmdb import TmdbSeason as _SchemaTmdbSeason
from app.adapters.network.http import RequestUtils
from app.domain.context import MediaInfo
from app.domain.media import is_media_source_enabled, is_media_source_selected
from app.domain.meta.metabase import MetaBase
from app.foundation.text import convert as zhconv_convert
from app.modules import _ModuleBase
from app.modules.themoviedb.category import CategoryHelper
from app.modules.themoviedb.scraper import TmdbScraper
from app.modules.themoviedb.tmdb_cache import TmdbCache
from app.modules.themoviedb.tmdbapi import TmdbApi
from app.modules.themoviedb.tmdbv3api.exceptions import TMDbConnectionError
from app.runtime.log import logger
from app.schemas.category import CategoryConfig
from app.schemas.media import normalize_media_source
from app.schemas.types import (
MediaImageType,
MediaRecognizeType,
MediaSource,
MediaSourceSelection,
MediaType,
ModuleType,
)
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
class TheMovieDbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
"""
TMDB媒体信息匹配
"""
CONFIG_WATCH = {"PROXY_HOST", "TMDB_API_DOMAIN", "TMDB_API_KEY", "TMDB_LOCALE"}
auxiliary_media_source = MediaSource.TMDB
# 元数据缓存
cache: TmdbCache = None
# TMDB
tmdb: TmdbApi = None
# 二级分类
category: CategoryHelper = None
# 刮削器
scraper: TmdbScraper = None
def init_module(self) -> None:
self.cache = TmdbCache()
self.tmdb = TmdbApi()
self.category = CategoryHelper()
self.scraper = TmdbScraper()
@staticmethod
def get_name() -> str:
return "TheMovieDb"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.MediaRecognize
@staticmethod
def get_subtype() -> MediaRecognizeType:
"""
获取模块子类型
"""
return MediaRecognizeType.TMDB
@staticmethod
def get_priority() -> int:
"""
获取模块优先级,数字越小优先级越高,只有同一接口下优先级才生效
"""
return 1
def stop(self) -> None:
"""停止模块"""
# 缓存持久化失败不能阻断 HTTP 客户端关闭
try:
self.cache.save()
finally:
self.tmdb.close()
def test(self) -> Tuple[bool, str]:
"""
测试模块连接性
"""
ret = RequestUtils(ua=get_runtime_setting("NORMAL_USER_AGENT"), proxies=get_runtime_setting("PROXY")).get_res(
f"https://{get_runtime_setting("TMDB_API_DOMAIN")}/3/movie/550?api_key={get_runtime_setting("TMDB_API_KEY")}")
if ret and ret.status_code == 200:
return True, ""
elif ret:
return False, f"无法连接 {get_runtime_setting("TMDB_API_DOMAIN")},错误码:{ret.status_code}"
return False, f"{get_runtime_setting("TMDB_API_DOMAIN")} 网络连接失败"
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass
@staticmethod
def _validate_recognize_params(
meta: MetaBase,
tmdbid: Optional[int],
media_source: Optional[MediaSource] = None,
) -> bool:
"""
验证识别参数
:param meta: 标题解析元数据
:param tmdbid: TMDB ID
:param media_source: 请求级识别数据源
:return: 参数是否可用于TMDB识别
"""
if not tmdbid and not meta:
return False
selected_source = normalize_media_source(media_source or get_runtime_setting("RECOGNIZE_SOURCE"))
if meta and not tmdbid and selected_source != MediaSource.TMDB:
return False
if meta and not meta.name and not tmdbid:
logger.warn("识别媒体信息时未提供元数据名称")
return False
return True
@staticmethod
def _prepare_search_names(meta: MetaBase) -> List[str]:
"""
准备搜索名称列表
"""
# 简体名称
zh_name = zhconv_convert(meta.cn_name, "zh-hans") if meta.cn_name else None
# 使用中英文名分别识别,去重去空,但要保持顺序
return list(dict.fromkeys([k for k in [meta.cn_name, zh_name, meta.en_name] if k]))
@staticmethod
def _fill_group_season_info(mediainfo: MediaInfo, episode_group: Optional[str],
group_seasons: List[dict]) -> None:
"""
将指定剧集组的季、集、年份信息写入 MediaInfo。
"""
seasons = {}
season_info = []
season_years = {}
for group_season in group_seasons:
# 季
season = group_season.get("order")
# 集列表
episodes = group_season.get("episodes")
if not episodes:
continue
seasons[season] = [ep.get("episode_number") for ep in episodes]
season_info.append(group_season)
# 当前季第一集时间
first_date = episodes[0].get("air_date")
if first_date and _DATE_RE.match(first_date):
season_years[season] = str(first_date).split("-")[0]
# 每季集清单
if seasons:
mediainfo.seasons = seasons
mediainfo.number_of_seasons = len(seasons)
# 每季集详情
if season_info:
mediainfo.season_info = season_info
# 每季年份
if season_years:
mediainfo.season_years = season_years
# 所有剧集组
mediainfo.episode_group = episode_group
mediainfo.episode_groups = group_seasons
@staticmethod
def _build_search_medias_result(meta: MetaBase, results: Optional[List[dict]]) -> List[MediaInfo]:
"""
构建搜索结果,并沿用原有逻辑把搜索词中的季写入电视剧标题中。
"""
if not results:
return []
medias = [MediaInfo(tmdb_info=info) for info in results]
if meta.begin_season is not None:
# 小写数据转大写
season_str = cn2an.an2cn(meta.begin_season, "low")
for media in medias:
if media.type == MediaType.TV:
media.title = f"{media.title}{season_str}季"
media.season = meta.begin_season
return medias
def _safe_get_info_by_type(self, mtype: MediaType, tmdbid: int) -> Tuple[Optional[dict], bool]:
"""
查询指定类型的媒体详情,将"确认TMDB连接失败"与"确认查无此项"区分开。
:param mtype: 媒体类型:电影或电视剧
:param tmdbid: TMDB的ID
:return: (媒体信息或None, 本次查询是否因TMDB连接失败而没有得到确定结果)
"""
try:
return self.tmdb.get_info(mtype=mtype, tmdbid=tmdbid, raise_on_connection_error=True), False
except TMDbConnectionError:
return None, True
async def _async_safe_get_info_by_type(self, mtype: MediaType, tmdbid: int) -> Tuple[Optional[dict], bool]:
"""
查询指定类型的媒体详情,将"确认TMDB连接失败"与"确认查无此项"区分开(异步版本)
"""
try:
return await self.tmdb.async_get_info(mtype=mtype, tmdbid=tmdbid, raise_on_connection_error=True), False
except TMDbConnectionError:
return None, True
def _get_info_by_tmdbid(self, tmdbid: int, mtype: Optional[MediaType],
meta: Optional[MetaBase]) -> Optional[dict]:
"""
根据tmdbid查询媒体信息,当类型未知且同时存在电影和电视剧时,通过元数据消歧
:raises TMDbConnectionError: 电影、电视剧两路查询都没有得到确定结果,且至少一路
是因TMDB连接失败导致的,此时不能断言"条目不存在",交由上层报网络故障
"""
if mtype:
return self.tmdb.get_info(mtype=mtype, tmdbid=tmdbid, raise_on_connection_error=True)
# 类型未知,分别查询电影和电视剧;每一路的连接失败要单独识别,
# 避免一路瞬时抖动掩盖另一路已经得到的确定结果
info_tv, tv_conn_error = self._safe_get_info_by_type(MediaType.TV, tmdbid)
info_movie, movie_conn_error = self._safe_get_info_by_type(MediaType.MOVIE, tmdbid)
if info_tv and info_movie:
# 同时存在,尝试通过元数据消歧
result = self._disambiguate_by_meta(info_tv, info_movie, meta)
if result:
return result
logger.warn(f"无法判断tmdb_id:{tmdbid} 是电影还是电视剧")
return None
if info_tv or info_movie:
return info_tv or info_movie
if tv_conn_error or movie_conn_error:
raise TMDbConnectionError(f"连接TheMovieDb失败,无法确认tmdb_id:{tmdbid} 的媒体类型")
return None
async def _async_get_info_by_tmdbid(self, tmdbid: int, mtype: Optional[MediaType],
meta: Optional[MetaBase]) -> Optional[dict]:
"""
根据tmdbid查询媒体信息,当类型未知且同时存在电影和电视剧时,通过元数据消歧(异步版本)
:raises TMDbConnectionError: 电影、电视剧两路查询都没有得到确定结果,且至少一路
是因TMDB连接失败导致的,此时不能断言"条目不存在",交由上层报网络故障
"""
if mtype:
return await self.tmdb.async_get_info(mtype=mtype, tmdbid=tmdbid, raise_on_connection_error=True)
# 类型未知,分别查询电影和电视剧;每一路的连接失败要单独识别,
# 避免一路瞬时抖动掩盖另一路已经得到的确定结果
info_tv, tv_conn_error = await self._async_safe_get_info_by_type(MediaType.TV, tmdbid)
info_movie, movie_conn_error = await self._async_safe_get_info_by_type(MediaType.MOVIE, tmdbid)
if info_tv and info_movie:
# 同时存在,尝试通过元数据消歧
result = self._disambiguate_by_meta(info_tv, info_movie, meta)
if result:
return result
logger.warn(f"无法判断tmdb_id:{tmdbid} 是电影还是电视剧")
return None
if info_tv or info_movie:
return info_tv or info_movie
if tv_conn_error or movie_conn_error:
raise TMDbConnectionError(f"连接TheMovieDb失败,无法确认tmdb_id:{tmdbid} 的媒体类型")
return None
@staticmethod
def _disambiguate_by_meta(info_tv: dict, info_movie: dict,
meta: Optional[MetaBase]) -> Optional[dict]:
"""
通过元数据(标题、年份、类型)对同tmdbid的电影和电视剧进行消歧
"""
if not meta:
return None
def _collect_titles(info: dict) -> set:
titles = set()
for key in ('title', 'name', 'original_title', 'original_name'):
if info.get(key):
titles.add(info[key])
for name in (info.get('names') or []):
titles.add(name)
return titles
def _match_score(info: dict) -> int:
score = 0
# 标题匹配
titles = _collect_titles(info)
meta_names = [n for n in [meta.cn_name, meta.en_name] if n]
for meta_name in meta_names:
if any(meta_name in t or t in meta_name for t in titles):
score += 2
break
# 年份匹配
if meta.year:
release_date = info.get('release_date') or info.get('first_air_date') or ''
if release_date and release_date[:4] == meta.year:
score += 1
return score
score_tv = _match_score(info_tv)
score_movie = _match_score(info_movie)
if score_tv > score_movie:
logger.info(f"通过元数据消歧,tmdb_id:{info_tv.get('id')} 识别为电视剧")
return info_tv
elif score_movie > score_tv:
logger.info(f"通过元数据消歧,tmdb_id:{info_movie.get('id')} 识别为电影")
return info_movie
# 评分相同时参考meta.type
if meta.type == MediaType.TV:
logger.info(f"通过媒体类型提示消歧,tmdb_id:{info_tv.get('id')} 识别为电视剧")
return info_tv
elif meta.type == MediaType.MOVIE:
logger.info(f"通过媒体类型提示消歧,tmdb_id:{info_movie.get('id')} 识别为电影")
return info_movie
return None
def _search_by_name(self, name: str, meta: MetaBase, group_seasons: List[dict]) -> dict:
"""
根据名称搜索媒体信息
"""
if meta.begin_season is not None:
logger.info(f"正在识别 {name}{meta.begin_season}季 ...")
else:
logger.info(f"正在识别 {name} ...")
if meta.type == MediaType.UNKNOWN and not meta.year:
return self.tmdb.match_multi(name)
else:
if meta.type == MediaType.TV:
# 确定是电视
info = self.tmdb.match(name=name,
year=meta.year,
mtype=meta.type,
season_year=meta.year,
season_number=meta.begin_season,
group_seasons=group_seasons)
if not info:
# 去掉年份再查一次
info = self.tmdb.match(name=name, mtype=meta.type)
return info
else:
# 有年份先按电影查
info = self.tmdb.match(name=name, year=meta.year, mtype=MediaType.MOVIE)
# 没有再按电视剧查
if not info:
info = self.tmdb.match(name=name, year=meta.year, mtype=MediaType.TV,
group_seasons=group_seasons)
if not info:
# 去掉年份和类型再查一次
info = self.tmdb.match_multi(name=name)
return info
async def _async_search_by_name(self, name: str, meta: MetaBase, group_seasons: List[dict]) -> dict:
"""
根据名称搜索媒体信息(异步版本)
"""
if meta.begin_season is not None:
logger.info(f"正在识别 {name}{meta.begin_season}季 ...")
else:
logger.info(f"正在识别 {name} ...")
if meta.type == MediaType.UNKNOWN and not meta.year:
return await self.tmdb.async_match_multi(name)
else:
if meta.type == MediaType.TV:
# 确定是电视
info = await self.tmdb.async_match(name=name,
year=meta.year,
mtype=meta.type,
season_year=meta.year,
season_number=meta.begin_season,
group_seasons=group_seasons)
if not info:
# 去掉年份再查一次
info = await self.tmdb.async_match(name=name, mtype=meta.type)
return info
else:
# 有年份先按电影查
info = await self.tmdb.async_match(name=name, year=meta.year, mtype=MediaType.MOVIE)
# 没有再按电视剧查
if not info:
info = await self.tmdb.async_match(name=name, year=meta.year, mtype=MediaType.TV,
group_seasons=group_seasons)
if not info:
# 去掉年份和类型再查一次
info = await self.tmdb.async_match_multi(name=name)
return info
def _process_episode_groups(self, mediainfo: MediaInfo, episode_group: Optional[str],
group_seasons: List[dict]) -> MediaInfo:
"""
处理剧集组信息
"""
if mediainfo.type == MediaType.TV and mediainfo.episode_groups:
if group_seasons:
self._fill_group_season_info(mediainfo, episode_group, group_seasons)
else:
# 每季年份
season_years = {}
for group in mediainfo.episode_groups:
if group.get('type') != 6:
# 只处理剧集部分
continue
group_episodes = self.tmdb.get_tv_group_seasons(group.get('id'))
if not group_episodes:
continue
for group_episode in group_episodes:
season = group_episode.get('order')
episodes = group_episode.get('episodes')
if not episodes:
continue
# 当前季第一季时间
first_date = episodes[0].get("air_date")
# 判断是不是日期格式
if first_date and _DATE_RE.match(first_date):
season_years[season] = str(first_date).split("-")[0]
if season_years:
mediainfo.season_years = season_years
return mediainfo
async def _async_process_episode_groups(self, mediainfo: MediaInfo, episode_group: Optional[str],
group_seasons: List[dict]) -> MediaInfo:
"""
处理剧集组信息(异步版本)
"""
if mediainfo.type == MediaType.TV and mediainfo.episode_groups:
if group_seasons:
self._fill_group_season_info(mediainfo, episode_group, group_seasons)
else:
# 每季年份
season_years = {}
for group in mediainfo.episode_groups:
if group.get('type') != 6:
# 只处理剧集部分
continue
group_episodes = await self.tmdb.async_get_tv_group_seasons(group.get('id'))
if not group_episodes:
continue
for group_episode in group_episodes:
season = group_episode.get('order')
episodes = group_episode.get('episodes')
if not episodes:
continue
# 当前季第一季时间
first_date = episodes[0].get("air_date")
# 判断是不是日期格式
if first_date and _DATE_RE.match(first_date):
season_years[season] = str(first_date).split("-")[0]
if season_years:
mediainfo.season_years = season_years
return mediainfo
def _build_media_info_result(self, info: dict, meta: MetaBase, tmdbid: Optional[int],
episode_group: Optional[str], group_seasons: List[dict]) -> MediaInfo:
"""
构建MediaInfo结果
"""
# 确定二级分类
if info.get('media_type') == MediaType.TV:
cat = self.category.get_tv_category(info)
else:
cat = self.category.get_movie_category(info)
# 赋值TMDB信息并返回
mediainfo = MediaInfo(tmdb_info=info)
mediainfo.set_category(cat)
if meta:
logger.info(f"{meta.name} TMDB识别结果:{mediainfo.type.value} "
f"{mediainfo.title_year} "
f"{mediainfo.tmdb_id}")
else:
logger.info(f"{tmdbid} TMDB识别结果:{mediainfo.type.value} "
f"{mediainfo.title_year}")
# 处理剧集组信息
return self._process_episode_groups(mediainfo, episode_group, group_seasons)
async def _async_build_media_info_result(self, info: dict, meta: MetaBase, tmdbid: Optional[int],
episode_group: Optional[str], group_seasons: List[dict]) -> MediaInfo:
"""
构建MediaInfo结果(异步版本)
"""
# 确定二级分类
if info.get('media_type') == MediaType.TV:
cat = self.category.get_tv_category(info)
else:
cat = self.category.get_movie_category(info)
# 赋值TMDB信息并返回
mediainfo = MediaInfo(tmdb_info=info)
mediainfo.set_category(cat)
if meta:
logger.info(f"{meta.name} TMDB识别结果:{mediainfo.type.value} "
f"{mediainfo.title_year} "
f"{mediainfo.tmdb_id}")
else:
logger.info(f"{tmdbid} TMDB识别结果:{mediainfo.type.value} "
f"{mediainfo.title_year}")
# 处理剧集组信息
return await self._async_process_episode_groups(mediainfo, episode_group, group_seasons)
def recognize_media(self, meta: MetaBase = None,
mtype: MediaType = None,
media_source: Optional[MediaSource] = None,
media_id: Optional[str] = None,
episode_group: Optional[str] = None,
cache: Optional[bool] = True,
**kwargs) -> Optional[MediaInfo]:
"""
识别媒体信息
:param meta: 识别的元数据
:param mtype: 识别的媒体类型
:param media_source: 媒体来源
:param media_id: 媒体来源原生ID
:param episode_group: 剧集组
:param cache: 是否使用缓存
:return: 识别的媒体信息,包括剧集信息
"""
# TMDB 只处理影视;音乐识别模块异常时也不能把音乐请求回退成电视剧搜索。
if mtype == MediaType.MUSIC or getattr(meta, "type", None) == MediaType.MUSIC:
return None
if media_source and media_source != MediaSource.TMDB:
return None
if media_id is not None and (
media_source != MediaSource.TMDB or not str(media_id).isdigit()
):
return None
tmdbid = int(media_id) if media_id is not None else None
# 验证参数
if not self._validate_recognize_params(meta, tmdbid, media_source):
return None
if not meta:
# 未提供元数据时,直接使用tmdbid查询,不使用缓存
cache_info = {}
else:
# 读取缓存
if mtype:
meta.type = mtype
if tmdbid:
meta.media_source = MediaSource.TMDB
meta.media_id = str(tmdbid)
cache_info = self.cache.get(meta) if cache else {}
# 查询剧集组
group_seasons = []
if episode_group:
group_seasons = self.tmdb.get_tv_group_seasons(episode_group)
cache_hit = False
# 识别匹配
if not cache_info or not cache:
info = None
connection_error = False
# 缓存没有或者强制不使用缓存
if tmdbid:
# 直接查询详情,支持同ID电影/电视剧消歧
try:
info = self._get_info_by_tmdbid(tmdbid=tmdbid, mtype=mtype, meta=meta)
except TMDbConnectionError as err:
logger.error(f"tmdb_id:{tmdbid} {err}")
connection_error = True
if not info and meta and not tmdbid:
# 准备搜索名称
names = self._prepare_search_names(meta)
for name in names:
info = self._search_by_name(name, meta, group_seasons)
if info:
# 查到就退出
break
# 补充全量信息
if info and not info.get("genres"):
info = self.tmdb.get_info(mtype=info.get("media_type"),
tmdbid=info.get("id"))
elif not info:
if connection_error:
# 网络故障与"条目不存在"是完全不同的两类问题,不能用同一句文案掩盖,
# 否则用户无从判断该等网络恢复还是该确认条目本身是否存在
logger.error(f"tmdb_id:{tmdbid} 连接TheMovieDb失败,无法完成识别,请检查网络连接后重试")
elif tmdbid:
logger.warn(f"tmdb_id:{tmdbid} 无法确定媒体类型,识别失败")
else:
logger.error("识别媒体信息时未提供元数据或唯一且有效的tmdbid")
return None
# 保存到缓存
if meta:
self.cache.update(meta, info)
else:
# 使用缓存信息
cache_hit = True
if cache_info.get("title"):
logger.info(f"{meta.name} 使用TMDB识别缓存:{cache_info.get('title')}")
info = self.tmdb.get_info(mtype=cache_info.get("type"),
tmdbid=cache_info.get("id"))
else:
logger.info(f"{meta.name} 使用TMDB识别缓存:无法识别")
info = None
if info:
mediainfo = self._build_media_info_result(info, meta, tmdbid, episode_group, group_seasons)
if mediainfo:
mediainfo.recognize_cache_hit = cache_hit
return mediainfo
else:
logger.info(f"{meta.name if meta else tmdbid} 未匹配到TMDB媒体信息")
return None
async def async_recognize_media(self, meta: MetaBase = None,
mtype: MediaType = None,
media_source: Optional[MediaSource] = None,
media_id: Optional[str] = None,
episode_group: Optional[str] = None,
cache: Optional[bool] = True,
**kwargs) -> Optional[MediaInfo]:
"""
识别媒体信息(异步版本)
:param meta: 识别的元数据
:param mtype: 识别的媒体类型
:param media_source: 媒体来源
:param media_id: 媒体来源原生ID
:param episode_group: 剧集组
:param cache: 是否使用缓存
:return: 识别的媒体信息,包括剧集信息
"""
# 与同步入口保持同一类型边界,音乐请求不得进入 TMDB。
if mtype == MediaType.MUSIC or getattr(meta, "type", None) == MediaType.MUSIC:
return None
if media_source and media_source != MediaSource.TMDB:
return None
if media_id is not None and (
media_source != MediaSource.TMDB or not str(media_id).isdigit()
):
return None
tmdbid = int(media_id) if media_id is not None else None
# 验证参数
if not self._validate_recognize_params(meta, tmdbid, media_source):
return None
if not meta:
# 未提供元数据时,直接使用tmdbid查询,不使用缓存
cache_info = {}
else:
# 读取缓存
if mtype:
meta.type = mtype
if tmdbid:
meta.media_source = MediaSource.TMDB
meta.media_id = str(tmdbid)
cache_info = self.cache.get(meta) if cache else {}
# 查询剧集组
group_seasons = []
if episode_group:
group_seasons = await self.tmdb.async_get_tv_group_seasons(episode_group)
cache_hit = False
# 识别匹配
if not cache_info or not cache:
info = None
connection_error = False
# 缓存没有或者强制不使用缓存
if tmdbid:
# 直接查询详情,支持同ID电影/电视剧消歧
try:
info = await self._async_get_info_by_tmdbid(tmdbid=tmdbid, mtype=mtype, meta=meta)
except TMDbConnectionError as err:
logger.error(f"tmdb_id:{tmdbid} {err}")
connection_error = True
if not info and meta and not tmdbid:
# 准备搜索名称
names = self._prepare_search_names(meta)
for name in names:
info = await self._async_search_by_name(name, meta, group_seasons)
if info:
# 查到就退出
break
# 补充全量信息
if info and not info.get("genres"):
info = await self.tmdb.async_get_info(mtype=info.get("media_type"),
tmdbid=info.get("id"))
elif not info:
if connection_error:
# 网络故障与"条目不存在"是完全不同的两类问题,不能用同一句文案掩盖,
# 否则用户无从判断该等网络恢复还是该确认条目本身是否存在
logger.error(f"tmdb_id:{tmdbid} 连接TheMovieDb失败,无法完成识别,请检查网络连接后重试")
elif tmdbid:
logger.warn(f"tmdb_id:{tmdbid} 无法确定媒体类型,识别失败")
else:
logger.error("识别媒体信息时未提供元数据或唯一且有效的tmdbid")
return None
# 保存到缓存
if meta:
self.cache.update(meta, info)
else:
# 使用缓存信息
cache_hit = True
if cache_info.get("title"):
logger.info(f"{meta.name} 使用TMDB识别缓存:{cache_info.get('title')}")
info = await self.tmdb.async_get_info(mtype=cache_info.get("type"),
tmdbid=cache_info.get("id"))
else:
logger.info(f"{meta.name} 使用TMDB识别缓存:无法识别")
info = None
if info:
mediainfo = await self._async_build_media_info_result(info, meta, tmdbid, episode_group, group_seasons)
if mediainfo:
mediainfo.recognize_cache_hit = cache_hit
return mediainfo
else:
logger.info(f"{meta.name if meta else tmdbid} 未匹配到TMDB媒体信息")
return None
def match_tmdbinfo(self, name: str, mtype: MediaType = None,
year: Optional[str] = None, season: Optional[int] = None) -> dict:
"""
搜索和匹配TMDB信息
:param name: 名称
:param mtype: 类型
:param year: 年份
:param season: 用于匹配指定季,0 表示特别季
"""
# 搜索
logger.info(f"开始使用 名称:{name} 年份:{year} 匹配TMDB信息 ...")
info = self.tmdb.match(name=name,
year=year,
mtype=mtype,
season_year=year,
season_number=season)
if info and not info.get("genres"):
info = self.tmdb.get_info(mtype=info.get("media_type"),
tmdbid=info.get("id"))
return info
async def async_match_tmdbinfo(self, name: str, mtype: MediaType = None,
year: Optional[str] = None, season: Optional[int] = None) -> dict:
"""
异步搜索和匹配TMDB信息
:param name: 名称
:param mtype: 类型
:param year: 年份
:param season: 用于匹配指定季,0 表示特别季
"""
# 搜索
logger.info(f"开始使用 名称:{name} 年份:{year} 匹配TMDB信息 ...")
info = await self.tmdb.async_match(name=name,
year=year,
mtype=mtype,
season_year=year,
season_number=season)
if info and not info.get("genres"):
info = await self.tmdb.async_get_info(mtype=info.get("media_type"),
tmdbid=info.get("id"))
return info
def tmdb_info(self, tmdbid: int, mtype: MediaType, season: Optional[int] = None) -> Optional[dict]:
"""
获取TMDB信息
:param tmdbid: int
:param mtype: 媒体类型
:param season: 季号;TV 的显式值(含 0)读取季详情,None 或电影的 0 读取媒体详情
:return: TMDB信息
"""
if season is None or (season == 0 and mtype != MediaType.TV):
return self.tmdb.get_info(mtype=mtype, tmdbid=tmdbid)
else:
return self.tmdb.get_tv_season_detail(tmdbid=tmdbid, season=season)
async def async_tmdb_info(self, tmdbid: int, mtype: MediaType, season: Optional[int] = None) -> Optional[dict]:
"""
异步获取TMDB信息
:param tmdbid: int
:param mtype: 媒体类型
:param season: 季号;TV 的显式值(含 0)读取季详情,None 或电影的 0 读取媒体详情
:return: TMDB信息
"""
if season is None or (season == 0 and mtype != MediaType.TV):
return await self.tmdb.async_get_info(mtype=mtype, tmdbid=tmdbid)
else:
return await self.tmdb.async_get_tv_season_detail(tmdbid=tmdbid, season=season)
def update_recognize_cache(
self,
meta: MetaBase,
mediainfo: MediaInfo,
) -> Optional[bool]:
"""
回填TMDB本地识别缓存,覆盖名称负缓存,避免共享识别后重复回查。
"""
if not meta or not mediainfo:
return None
if mediainfo.media_source != "themoviedb" or not mediainfo.tmdb_info:
return None
self.cache.update(meta, mediainfo.tmdb_info)
return True
async def async_update_recognize_cache(
self,
meta: MetaBase,
mediainfo: MediaInfo,
) -> Optional[bool]:
"""
异步回填TMDB本地识别缓存。
"""
return self.update_recognize_cache(meta=meta, mediainfo=mediainfo)
def tmdb_cache_items(self) -> list:
"""
查询TMDB识别缓存条目列表
"""
return self.cache.list_items()
def tmdb_cache_delete(self, cache_key: str) -> dict:
"""
按缓存键删除单条TMDB识别缓存
"""
return self.cache.delete(cache_key)
def tmdb_cache_clear(self) -> None:
"""
清空全部TMDB识别缓存
"""
self.cache.clear()
def media_category(self) -> Optional[Dict[str, list]]:
"""
获取媒体分类
:return: 获取二级分类配置字典项,需包括电影、电视剧
"""
return {
MediaType.MOVIE.value: list(self.category.movie_categorys),
MediaType.TV.value: list(self.category.tv_categorys)
}
def search_medias(
self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
) -> Optional[List[MediaInfo]]:
"""
搜索媒体信息
:param meta: 识别的元数据
:param media_source: 请求级搜索数据源
:return: 媒体信息列表
"""
if not is_media_source_enabled(media_source, MediaSource.TMDB):
return None
if not meta.name:
return []
if meta.type == MediaType.UNKNOWN and not meta.year:
results = self.tmdb.search_multiis(meta.name)
else:
if meta.type == MediaType.UNKNOWN:
results = self.tmdb.search_movies(meta.name, meta.year)
results.extend(self.tmdb.search_tvs(meta.name, meta.year))
# 组合结果的情况下要排序
results = sorted(
results,
key=lambda x: x.get("release_date") or x.get("first_air_date") or "0000-00-00",
reverse=True
)
elif meta.type == MediaType.MOVIE:
results = self.tmdb.search_movies(meta.name, meta.year)
else:
results = self.tmdb.search_tvs(meta.name, meta.year)
# 将搜索词中的季写入标题中
return self._build_search_medias_result(meta, results)
def search_persons(
self, name: str, media_source: Optional[MediaSourceSelection] = None
) -> Optional[List[_SchemaMediaPerson]]:
"""
搜索人物信息
:param name: 人物名称
:param media_source: 请求级搜索数据源
:return: 人物信息列表
"""
if not is_media_source_enabled(media_source, MediaSource.TMDB):
return None
if not name:
return []
results = self.tmdb.search_persons(name)
if results:
return [_SchemaMediaPerson(source='themoviedb', **person) for person in results]
return []
async def async_search_persons(
self, name: str, media_source: Optional[MediaSourceSelection] = None
) -> Optional[List[_SchemaMediaPerson]]:
"""
异步搜索人物信息
:param name: 人物名称
:param media_source: 请求级搜索数据源
:return: 人物信息列表
"""
if not is_media_source_enabled(media_source, MediaSource.TMDB):
return None
if not name:
return []
results = await self.tmdb.async_search_persons(name)
if results:
return [_SchemaMediaPerson(source='themoviedb', **person) for person in results]
return []
def search_collections(
self, name: str, media_source: Optional[MediaSourceSelection] = None
) -> Optional[List[MediaInfo]]:
"""
搜索集合信息
:param name: 合集名称
:param media_source: 请求级搜索数据源
:return: 合集信息列表
"""
if media_source and not is_media_source_selected(media_source, MediaSource.TMDB):
return None
if not name:
return []
results = self.tmdb.search_collections(name)
if results:
return [MediaInfo(tmdb_info=info) for info in results]
return []
async def async_search_collections(
self, name: str, media_source: Optional[MediaSourceSelection] = None
) -> Optional[List[MediaInfo]]:
"""
异步搜索集合信息
:param name: 合集名称
:param media_source: 请求级搜索数据源
:return: 合集信息列表
"""
if media_source and not is_media_source_selected(media_source, MediaSource.TMDB):
return None
if not name:
return []
results = await self.tmdb.async_search_collections(name)
if results:
return [MediaInfo(tmdb_info=info) for info in results]
return []
def tmdb_collection(self, collection_id: int) -> Optional[List[MediaInfo]]:
"""
根据合集ID查询集合
:param collection_id: 合集ID
"""
results = self.tmdb.get_collection(collection_id)
if results:
return [MediaInfo(tmdb_info=info) for info in results]
return []
def metadata_nfo(self, meta: MetaBase, mediainfo: MediaInfo,
season: Optional[int] = None, episode: Optional[int] = None) -> Optional[str]:
"""
获取NFO文件内容文本
:param meta: 元数据
:param mediainfo: 媒体信息
:param season: 季号
:param episode: 集号
"""
if (mediainfo.scrape_source or get_runtime_setting("SCRAP_SOURCE")) != "themoviedb":
return None
return self.scraper.get_metadata_nfo(meta=meta, mediainfo=mediainfo, season=season, episode=episode)
def metadata_img(self, mediainfo: MediaInfo, season: Optional[int] = None,
episode: Optional[int] = None) -> Optional[dict]:
"""
获取图片名称和url
:param mediainfo: 媒体信息
:param season: 季号
:param episode: 集号
"""
if (mediainfo.scrape_source or get_runtime_setting("SCRAP_SOURCE")) != "themoviedb":
return None
return self.scraper.get_metadata_img(mediainfo=mediainfo, season=season, episode=episode)
def tmdb_discover(self, mtype: MediaType, sort_by: str,
with_genres: str,
with_original_language: str,
with_keywords: str,
with_watch_providers: str,
vote_average: float,
vote_count: int,
release_date: str,
page: Optional[int] = 1) -> Optional[List[MediaInfo]]:
"""
:param mtype: 媒体类型
:param sort_by: 排序方式
:param with_genres: 类型
:param with_original_language: 语言
:param with_keywords: 关键字
:param with_watch_providers: 提供商
:param vote_average: 评分
:param vote_count: 评分人数
:param release_date: 发布日期
:param page: 页码
:return: 媒体信息列表
"""
if mtype == MediaType.MOVIE:
infos = self.tmdb.discover_movies({
"sort_by": sort_by,
"with_genres": with_genres,
"with_original_language": with_original_language,
"with_keywords": with_keywords,
"with_watch_providers": with_watch_providers,
"vote_average.gte": vote_average,
"vote_count.gte": vote_count,
"release_date.gte": release_date,
"page": page
})
elif mtype == MediaType.TV:
infos = self.tmdb.discover_tvs({
"sort_by": sort_by,
"with_genres": with_genres,
"with_original_language": with_original_language,
"with_keywords": with_keywords,
"with_watch_providers": with_watch_providers,
"vote_average.gte": vote_average,
"vote_count.gte": vote_count,
"first_air_date.gte": release_date,
"page": page
})
else:
return []
if infos:
return [MediaInfo(tmdb_info=info) for info in infos]
return []
def tmdb_trending(self, page: Optional[int] = 1) -> List[MediaInfo]:
"""
TMDB流行趋势
:param page: 第几页
:return: TMDB信息列表
"""
trending = self.tmdb.discover_trending(page=page)
if trending:
return [MediaInfo(tmdb_info=info) for info in trending]
return []
def tmdb_seasons(self, tmdbid: int) -> List[_SchemaTmdbSeason]:
"""
根据TMDBID查询themoviedb所有季信息
:param tmdbid: TMDBID
"""
tmdb_info = self.tmdb.get_info(tmdbid=tmdbid, mtype=MediaType.TV)
if not tmdb_info:
return []
return [_SchemaTmdbSeason(**sea)
for sea in tmdb_info.get("seasons", []) if sea.get("season_number") is not None]
def tmdb_group_seasons(self, group_id: str) -> List[_SchemaTmdbSeason]:
"""
根据剧集组ID查询themoviedb所有季集信息
:param group_id: 剧集组ID
"""
group_seasons = self.tmdb.get_tv_group_seasons(group_id)
if not group_seasons:
return []
return [_SchemaTmdbSeason(
season_number=sea.get("order"),
name=sea.get("name"),
episode_count=len(sea.get("episodes") or []),
air_date=sea.get("episodes")[0].get("air_date") if sea.get("episodes") else None,
) for sea in group_seasons]
def tmdb_episodes(self, tmdbid: int, season: int, episode_group: Optional[str] = None) -> List[_SchemaTmdbEpisode]:
"""
根据TMDBID查询某季的所有集信息
:param tmdbid: TMDBID
:param season: 季
:param episode_group: 剧集组
"""
if episode_group:
season_info = self.tmdb.get_tv_group_detail(episode_group, season=season)
else:
season_info = self.tmdb.get_tv_season_detail(tmdbid=tmdbid, season=season)
if not season_info or not season_info.get("episodes"):
return []
return [_SchemaTmdbEpisode(**episode) for episode in season_info.get("episodes")]
def scheduler_job(self) -> None:
"""
定时任务,每10分钟调用一次
"""
self.cache.save()
@staticmethod
def _validate_obtain_images_params(mediainfo: MediaInfo) -> Optional[MediaInfo]:
"""
验证 obtain_images 参数
:param mediainfo: 媒体信息
:return: None 表示不处理,MediaInfo 表示继续处理
"""
if mediainfo.media_source != "themoviedb" and get_runtime_setting("RECOGNIZE_SOURCE") != "themoviedb":
return None
if not mediainfo.tmdb_id:
return mediainfo
if mediainfo.logo_path \
and mediainfo.poster_path \
and mediainfo.backdrop_path:
# 没有图片缺失
return mediainfo
return None
@staticmethod
def _pick_best_tmdb_image(images: list) -> Optional[str]:
"""
从 TMDB 图片候选中选出评分最高的文件路径。
"""
if not images:
return None
images = sorted(
images,
key=lambda x: (
x.get("vote_average") or 0,
x.get("vote_count") or 0,
),
reverse=True,
)
return images[0].get("file_path")
@classmethod
def _process_tmdb_images(cls, mediainfo: MediaInfo, images: dict) -> MediaInfo:
"""
处理 TMDB 图片数据
:param mediainfo: 媒体信息
:param images: 图片数据
:return: 更新后的媒体信息
"""
if isinstance(images, list):
images = images[0]
# 背景图
if not mediainfo.backdrop_path:
if image_path := cls._pick_best_tmdb_image(images.get("backdrops")):
mediainfo.backdrop_path = get_runtime_setting("TMDB_IMAGE_URL")(image_path)
# 标志
if not mediainfo.logo_path:
if image_path := cls._pick_best_tmdb_image(images.get("logos")):
mediainfo.logo_path = get_runtime_setting("TMDB_IMAGE_URL")(image_path)
# 海报
if not mediainfo.poster_path:
if image_path := cls._pick_best_tmdb_image(images.get("posters")):
mediainfo.poster_path = get_runtime_setting("TMDB_IMAGE_URL")(image_path)
return mediainfo
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
"""
补充抓取媒体信息图片
:param mediainfo: 识别的媒体信息
:return: 更新后的媒体信息
"""
# 验证参数
result = self._validate_obtain_images_params(mediainfo)
if result is not None:
return result
# 调用TMDB图片接口
if mediainfo.type == MediaType.MOVIE:
images = self.tmdb.get_movie_images(
mediainfo.tmdb_id,
original_language=mediainfo.original_language,
)
else:
images = self.tmdb.get_tv_images(
mediainfo.tmdb_id,
original_language=mediainfo.original_language,
)
if not images:
return mediainfo
# 处理图片数据
return self._process_tmdb_images(mediainfo, images)
async def async_obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
"""
补充抓取媒体信息图片(异步版本)
:param mediainfo: 识别的媒体信息
:return: 更新后的媒体信息
"""
# 验证参数
result = self._validate_obtain_images_params(mediainfo)
if result is not None:
return result
# 调用TMDB图片接口
if mediainfo.type == MediaType.MOVIE:
images = await self.tmdb.async_get_movie_images(
mediainfo.tmdb_id,
original_language=mediainfo.original_language,
)
else:
images = await self.tmdb.async_get_tv_images(
mediainfo.tmdb_id,
original_language=mediainfo.original_language,
)
if not images:
return mediainfo
# 处理图片数据
return self._process_tmdb_images(mediainfo, images)
def obtain_specific_image(self, mediaid: Union[str, int], mtype: MediaType,
image_type: MediaImageType, image_prefix: Optional[str] = "w500",
season: Optional[int] = None, episode: Optional[int] = None) -> Optional[str]:
"""
获取指定媒体信息图片,返回图片地址
:param mediaid: 媒体ID
:param mtype: 媒体类型
:param image_type: 图片类型
:param image_prefix: 图片前缀
:param season: 季
:param episode: 集
"""
if not str(mediaid).isdigit():
return None
# 图片相对路径
image_path = None
image_prefix = image_prefix or "w500"
if season is None and not episode:
tmdbinfo = self.tmdb.get_info(mtype=mtype, tmdbid=int(mediaid))
if tmdbinfo:
image_path = tmdbinfo.get(image_type.value)
elif season is not None and episode:
episodeinfo = self.tmdb.get_tv_episode_detail(tmdbid=int(mediaid), season=season, episode=episode)
if episodeinfo:
image_path = episodeinfo.get("still_path")
elif season is not None:
seasoninfo = self.tmdb.get_tv_season_detail(tmdbid=int(mediaid), season=season)
if seasoninfo:
image_path = seasoninfo.get(image_type.value)
if image_path:
return get_runtime_setting("TMDB_IMAGE_URL")(image_path, image_prefix)
return None
def tmdb_movie_similar(self, tmdbid: int) -> List[MediaInfo]:
"""
根据TMDBID查询类似电影
:param tmdbid: TMDBID
"""
similar = self.tmdb.get_movie_similar(tmdbid=tmdbid)
if similar:
return [MediaInfo(tmdb_info=info) for info in similar]
return []
def tmdb_tv_similar(self, tmdbid: int) -> List[MediaInfo]:
"""
根据TMDBID查询类似电视剧
:param tmdbid: TMDBID
"""
similar = self.tmdb.get_tv_similar(tmdbid=tmdbid)
if similar:
return [MediaInfo(tmdb_info=info) for info in similar]
return []
def tmdb_movie_recommend(self, tmdbid: int) -> List[MediaInfo]:
"""
根据TMDBID查询推荐电影
:param tmdbid: TMDBID
"""
recommend = self.tmdb.get_movie_recommend(tmdbid=tmdbid)
if recommend:
return [MediaInfo(tmdb_info=info) for info in recommend]
return []
def tmdb_tv_recommend(self, tmdbid: int) -> List[MediaInfo]:
"""
根据TMDBID查询推荐电视剧
:param tmdbid: TMDBID
"""
recommend = self.tmdb.get_tv_recommend(tmdbid=tmdbid)
if recommend:
return [MediaInfo(tmdb_info=info) for info in recommend]
return []
def tmdb_movie_credits(self, tmdbid: int, page: Optional[int] = 1) -> List[_SchemaMediaPerson]:
"""
根据TMDBID查询电影演职员表
:param tmdbid: TMDBID
:param page: 页码
"""
credit_infos = self.tmdb.get_movie_credits(tmdbid=tmdbid, page=page)
if credit_infos:
return [_SchemaMediaPerson(source="themoviedb", **info) for info in credit_infos]
return []
def tmdb_tv_credits(self, tmdbid: int, page: Optional[int] = 1) -> List[_SchemaMediaPerson]:
"""
根据TMDBID查询电视剧演职员表
:param tmdbid: TMDBID
:param page: 页码
"""
credit_infos = self.tmdb.get_tv_credits(tmdbid=tmdbid, page=page)
if credit_infos:
return [_SchemaMediaPerson(source="themoviedb", **info) for info in credit_infos]
return []
def tmdb_person_detail(self, person_id: int) -> _SchemaMediaPerson:
"""
根据TMDBID查询人物详情
:param person_id: 人物ID
"""
detail = self.tmdb.get_person_detail(person_id=person_id)
if detail:
return _SchemaMediaPerson(source="themoviedb", **detail)
return _SchemaMediaPerson()
def tmdb_person_credits(self, person_id: int, page: Optional[int] = 1) -> List[MediaInfo]:
"""
根据TMDBID查询人物参演作品
:param person_id: 人物ID
:param page: 页码
"""
infos = self.tmdb.get_person_credits(person_id=person_id, page=page)
if infos:
return [MediaInfo(tmdb_info=tmdbinfo) for tmdbinfo in infos]
return []
# 异步方法
async def async_search_medias(
self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
) -> Optional[List[MediaInfo]]:
"""
搜索媒体信息(异步版本)
:param meta: 识别的元数据
:param media_source: 请求级搜索数据源
:return: 媒体信息列表
"""
if not is_media_source_enabled(media_source, MediaSource.TMDB):
return None
if not meta.name:
return []
if meta.type == MediaType.UNKNOWN and not meta.year:
results = await self.tmdb.async_search_multiis(meta.name)
else:
if meta.type == MediaType.UNKNOWN:
results = await self.tmdb.async_search_movies(meta.name, meta.year)
results.extend(await self.tmdb.async_search_tvs(meta.name, meta.year))
# 组合结果的情况下要排序
results = sorted(
results,
key=lambda x: x.get("release_date") or x.get("first_air_date") or "0000-00-00",
reverse=True
)
elif meta.type == MediaType.MOVIE:
results = await self.tmdb.async_search_movies(meta.name, meta.year)
else:
results = await self.tmdb.async_search_tvs(meta.name, meta.year)
# 将搜索词中的季写入标题中
return self._build_search_medias_result(meta, results)
async def async_tmdb_discover(self, mtype: MediaType, sort_by: str,
with_genres: str,
with_original_language: str,
with_keywords: str,
with_watch_providers: str,
vote_average: float,
vote_count: int,
release_date: str,
page: Optional[int] = 1,
raise_exception: bool = False) -> Optional[List[MediaInfo]]:
"""
TMDB发现功能(异步版本)
:param mtype: 媒体类型
:param sort_by: 排序方式
:param with_genres: 类型
:param with_original_language: 语言
:param with_keywords: 关键字
:param with_watch_providers: 提供商
:param vote_average: 评分
:param vote_count: 评分人数
:param release_date: 发布日期
:param page: 页码
:return: 媒体信息列表
"""
if mtype == MediaType.MOVIE:
infos = await self.tmdb.async_discover_movies({
"sort_by": sort_by,
"with_genres": with_genres,
"with_original_language": with_original_language,
"with_keywords": with_keywords,
"with_watch_providers": with_watch_providers,
"vote_average.gte": vote_average,
"vote_count.gte": vote_count,
"release_date.gte": release_date,
"page": page
}, raise_exception=raise_exception)
elif mtype == MediaType.TV:
infos = await self.tmdb.async_discover_tvs({
"sort_by": sort_by,
"with_genres": with_genres,
"with_original_language": with_original_language,
"with_keywords": with_keywords,
"with_watch_providers": with_watch_providers,
"vote_average.gte": vote_average,
"vote_count.gte": vote_count,
"first_air_date.gte": release_date,
"page": page
}, raise_exception=raise_exception)
else:
return []
if infos:
return [MediaInfo(tmdb_info=info) for info in infos]
return []
async def async_tmdb_trending(
self, page: Optional[int] = 1, raise_exception: bool = False
) -> List[MediaInfo]:
"""
TMDB流行趋势(异步版本)
:param page: 第几页
:return: TMDB信息列表
"""
trending = await self.tmdb.async_discover_trending(
page=page,
raise_exception=raise_exception,
)
if trending:
return [MediaInfo(tmdb_info=info) for info in trending]
return []
async def async_tmdb_collection(self, collection_id: int) -> Optional[List[MediaInfo]]:
"""
根据合集ID查询集合(异步版本)
:param collection_id: 合集ID
"""
results = await self.tmdb.async_get_collection(collection_id)
if results:
return [MediaInfo(tmdb_info=info) for info in results]
return []
async def async_tmdb_seasons(self, tmdbid: int) -> List[_SchemaTmdbSeason]:
"""
根据TMDBID查询themoviedb所有季信息(异步版本)
:param tmdbid: TMDBID
"""
tmdb_info = await self.tmdb.async_get_info(tmdbid=tmdbid, mtype=MediaType.TV)
if not tmdb_info:
return []
return [_SchemaTmdbSeason(**sea)
for sea in tmdb_info.get("seasons", []) if sea.get("season_number") is not None]
async def async_tmdb_group_seasons(self, group_id: str) -> List[_SchemaTmdbSeason]:
"""
根据剧集组ID查询themoviedb所有季集信息(异步版本)
:param group_id: 剧集组ID
"""
group_seasons = await self.tmdb.async_get_tv_group_seasons(group_id)
if not group_seasons:
return []
return [_SchemaTmdbSeason(
season_number=sea.get("order"),
name=sea.get("name"),
episode_count=len(sea.get("episodes") or []),
air_date=sea.get("episodes")[0].get("air_date") if sea.get("episodes") else None,
) for sea in group_seasons]
async def async_tmdb_episodes(self, tmdbid: int, season: int,
episode_group: Optional[str] = None) -> List[_SchemaTmdbEpisode]:
"""
根据TMDBID查询某季的所有集信息(异步版本)
:param tmdbid: TMDBID
:param season: 季
:param episode_group: 剧集组
"""
if episode_group:
season_info = await self.tmdb.async_get_tv_group_detail(episode_group, season=season)
else:
season_info = await self.tmdb.async_get_tv_season_detail(tmdbid=tmdbid, season=season)
if not season_info or not season_info.get("episodes"):
return []
return [_SchemaTmdbEpisode(**episode) for episode in season_info.get("episodes")]
async def async_tmdb_movie_similar(self, tmdbid: int) -> List[MediaInfo]:
"""
根据TMDBID查询类似电影(异步版本)
:param tmdbid: TMDBID
"""
similar = await self.tmdb.async_get_movie_similar(tmdbid=tmdbid)
if similar:
return [MediaInfo(tmdb_info=info) for info in similar]
return []
async def async_tmdb_tv_similar(self, tmdbid: int) -> List[MediaInfo]:
"""
根据TMDBID查询类似电视剧(异步版本)
:param tmdbid: TMDBID
"""
similar = await self.tmdb.async_get_tv_similar(tmdbid=tmdbid)
if similar:
return [MediaInfo(tmdb_info=info) for info in similar]
return []
async def async_tmdb_movie_recommend(self, tmdbid: int) -> List[MediaInfo]:
"""
根据TMDBID查询推荐电影(异步版本)
:param tmdbid: TMDBID
"""
recommend = await self.tmdb.async_get_movie_recommend(tmdbid=tmdbid)
if recommend:
return [MediaInfo(tmdb_info=info) for info in recommend]
return []
async def async_tmdb_tv_recommend(self, tmdbid: int) -> List[MediaInfo]:
"""
根据TMDBID查询推荐电视剧(异步版本)
:param tmdbid: TMDBID
"""
recommend = await self.tmdb.async_get_tv_recommend(tmdbid=tmdbid)
if recommend:
return [MediaInfo(tmdb_info=info) for info in recommend]
return []
async def async_tmdb_movie_credits(self, tmdbid: int, page: Optional[int] = 1) -> List[_SchemaMediaPerson]:
"""
根据TMDBID查询电影演职员表(异步版本)
:param tmdbid: TMDBID
:param page: 页码
"""
credit_infos = await self.tmdb.async_get_movie_credits(tmdbid=tmdbid, page=page)
if credit_infos:
return [_SchemaMediaPerson(source="themoviedb", **info) for info in credit_infos]
return []
async def async_tmdb_tv_credits(self, tmdbid: int, page: Optional[int] = 1) -> List[_SchemaMediaPerson]:
"""
根据TMDBID查询电视剧演职员表(异步版本)
:param tmdbid: TMDBID
:param page: 页码
"""
credit_infos = await self.tmdb.async_get_tv_credits(tmdbid=tmdbid, page=page)
if credit_infos:
return [_SchemaMediaPerson(source="themoviedb", **info) for info in credit_infos]
return []
async def async_tmdb_person_detail(self, person_id: int) -> _SchemaMediaPerson:
"""
根据TMDBID查询人物详情(异步版本)
:param person_id: 人物ID
"""
detail = await self.tmdb.async_get_person_detail(person_id=person_id)
if detail:
return _SchemaMediaPerson(source="themoviedb", **detail)
return _SchemaMediaPerson()
async def async_tmdb_person_credits(self, person_id: int, page: Optional[int] = 1) -> List[MediaInfo]:
"""
根据TMDBID查询人物参演作品(异步版本)
:param person_id: 人物ID
:param page: 页码
"""
infos = await self.tmdb.async_get_person_credits(person_id=person_id, page=page)
if infos:
return [MediaInfo(tmdb_info=tmdbinfo) for tmdbinfo in infos]
return []
def clear_cache(self):
"""
清除缓存
"""
logger.info("开始清除TMDB缓存 ...")
self.tmdb.clear_cache()
self.cache.clear()
logger.info("TMDB缓存清除完成")
def load_category_config(self) -> CategoryConfig:
"""
加载分类配置
"""
return self.category.load()
def save_category_config(self, config: CategoryConfig) -> bool:
"""
保存分类配置
"""
return self.category.save(config)