mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
e02418bac 把新导入放在 settings = RuntimeSettingsCompat() 之后,
导致 5 个模块 E402 诊断超出只降不增基线;同时 media_auxiliary
把工厂函数 MetaInfo 误用作返回类型标注。将新增导入移回赋值语句
之前,返回类型修正为 MetaBase。
1592 lines
64 KiB
Python
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)
|