596 lines
23 KiB
Python
596 lines
23 KiB
Python
from pathlib import Path
|
||
from typing import Optional, List, Tuple, Union, Dict
|
||
|
||
import cn2an
|
||
|
||
from app import schemas
|
||
from app.core.config import settings
|
||
from app.core.context import MediaInfo
|
||
from app.core.meta import MetaBase
|
||
from app.log import logger
|
||
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.schemas import MediaPerson
|
||
from app.schemas.types import MediaType, MediaImageType
|
||
from app.utils.http import RequestUtils
|
||
from app.utils.system import SystemUtils
|
||
|
||
|
||
class TheMovieDbModule(_ModuleBase):
|
||
"""
|
||
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(self.tmdb)
|
||
|
||
@staticmethod
|
||
def get_name() -> str:
|
||
return "TheMovieDb"
|
||
|
||
def stop(self):
|
||
self.cache.save()
|
||
self.tmdb.close()
|
||
|
||
def test(self) -> Tuple[bool, str]:
|
||
"""
|
||
测试模块连接性
|
||
"""
|
||
ret = RequestUtils(proxies=settings.PROXY).get_res(
|
||
f"https://{settings.TMDB_API_DOMAIN}/3/movie/550?api_key={settings.TMDB_API_KEY}")
|
||
if ret and ret.status_code == 200:
|
||
return True, ""
|
||
elif ret:
|
||
return False, f"无法连接 {settings.TMDB_API_DOMAIN},错误码:{ret.status_code}"
|
||
return False, f"{settings.TMDB_API_DOMAIN} 网络连接失败"
|
||
|
||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||
pass
|
||
|
||
def recognize_media(self, meta: MetaBase = None,
|
||
mtype: MediaType = None,
|
||
tmdbid: int = None,
|
||
cache: bool = True,
|
||
**kwargs) -> Optional[MediaInfo]:
|
||
"""
|
||
识别媒体信息
|
||
:param meta: 识别的元数据
|
||
:param mtype: 识别的媒体类型,与tmdbid配套
|
||
:param tmdbid: tmdbid
|
||
:param cache: 是否使用缓存
|
||
:return: 识别的媒体信息,包括剧集信息
|
||
"""
|
||
if not tmdbid and not meta:
|
||
return None
|
||
|
||
if meta and not tmdbid \
|
||
and settings.RECOGNIZE_SOURCE != "themoviedb":
|
||
return None
|
||
|
||
if not meta:
|
||
# 未提供元数据时,直接使用tmdbid查询,不使用缓存
|
||
cache_info = {}
|
||
elif not meta.name:
|
||
logger.warn("识别媒体信息时未提供元数据名称")
|
||
return None
|
||
else:
|
||
# 读取缓存
|
||
if mtype:
|
||
meta.type = mtype
|
||
if tmdbid:
|
||
meta.tmdbid = tmdbid
|
||
cache_info = self.cache.get(meta)
|
||
|
||
# 识别匹配
|
||
if not cache_info or not cache:
|
||
# 缓存没有或者强制不使用缓存
|
||
if tmdbid:
|
||
# 直接查询详情
|
||
info = self.tmdb.get_info(mtype=mtype, tmdbid=tmdbid)
|
||
elif meta:
|
||
info = {}
|
||
# 使用中英文名分别识别,去重去空,但要保持顺序
|
||
names = list(dict.fromkeys([k for k in [meta.cn_name, meta.en_name] if k]))
|
||
for name in names:
|
||
if meta.begin_season:
|
||
logger.info(f"正在识别 {name} 第{meta.begin_season}季 ...")
|
||
else:
|
||
logger.info(f"正在识别 {name} ...")
|
||
if meta.type == MediaType.UNKNOWN and not meta.year:
|
||
info = 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)
|
||
if not info:
|
||
# 去掉年份再查一次
|
||
info = self.tmdb.match(name=name,
|
||
mtype=meta.type)
|
||
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)
|
||
if not info:
|
||
# 去掉年份和类型再查一次
|
||
info = self.tmdb.match_multi(name=name)
|
||
|
||
if not info:
|
||
# 从网站查询
|
||
info = self.tmdb.match_web(name=name,
|
||
mtype=meta.type)
|
||
if info:
|
||
# 查到就退出
|
||
break
|
||
# 补充全量信息
|
||
if info and not info.get("genres"):
|
||
info = self.tmdb.get_info(mtype=info.get("media_type"),
|
||
tmdbid=info.get("id"))
|
||
else:
|
||
logger.error("识别媒体信息时未提供元数据或tmdbid")
|
||
return None
|
||
|
||
# 保存到缓存
|
||
if meta:
|
||
self.cache.update(meta, info)
|
||
else:
|
||
# 使用缓存信息
|
||
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:
|
||
# 确定二级分类
|
||
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}")
|
||
|
||
# 补充剧集年份
|
||
if mediainfo.type == MediaType.TV:
|
||
episode_years = self.tmdb.get_tv_episode_years(info.get("id"))
|
||
if episode_years:
|
||
mediainfo.season_years = episode_years
|
||
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: str = None, season: int = None) -> dict:
|
||
"""
|
||
搜索和匹配TMDB信息
|
||
:param name: 名称
|
||
:param mtype: 类型
|
||
:param year: 年份
|
||
:param season: 季号
|
||
"""
|
||
# 搜索
|
||
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
|
||
|
||
def tmdb_info(self, tmdbid: int, mtype: MediaType, season: int = None) -> Optional[dict]:
|
||
"""
|
||
获取TMDB信息
|
||
:param tmdbid: int
|
||
:param mtype: 媒体类型
|
||
:param season: 季号
|
||
:return: TVDB信息
|
||
"""
|
||
if not season:
|
||
return self.tmdb.get_info(mtype=mtype, tmdbid=tmdbid)
|
||
else:
|
||
return self.tmdb.get_tv_season_detail(tmdbid=tmdbid, season=season)
|
||
|
||
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) -> Optional[List[MediaInfo]]:
|
||
"""
|
||
搜索媒体信息
|
||
:param meta: 识别的元数据
|
||
:reutrn: 媒体信息列表
|
||
"""
|
||
if settings.SEARCH_SOURCE and "themoviedb" not in settings.SEARCH_SOURCE:
|
||
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)
|
||
# 将搜索词中的季写入标题中
|
||
if results:
|
||
medias = [MediaInfo(tmdb_info=info) for info in results]
|
||
if meta.begin_season:
|
||
# 小写数据转大写
|
||
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
|
||
return []
|
||
|
||
def search_persons(self, name: str) -> Optional[List[MediaPerson]]:
|
||
"""
|
||
搜索人物信息
|
||
"""
|
||
if not name:
|
||
return []
|
||
results = self.tmdb.search_persons(name)
|
||
if results:
|
||
return [MediaPerson(source='themoviedb', **person) for person in results]
|
||
return []
|
||
|
||
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str,
|
||
metainfo: MetaBase = None, force_nfo: bool = False, force_img: bool = False) -> None:
|
||
"""
|
||
刮削元数据
|
||
:param path: 媒体文件路径
|
||
:param mediainfo: 识别的媒体信息
|
||
:param metainfo: 源文件的识别元数据
|
||
:param transfer_type: 转移类型
|
||
:param force_nfo: 强制刮削nfo
|
||
:param force_img: 强制刮削图片
|
||
:return: 成功或失败
|
||
"""
|
||
if settings.SCRAP_SOURCE != "themoviedb":
|
||
return None
|
||
|
||
if SystemUtils.is_bluray_dir(path):
|
||
# 蓝光原盘
|
||
logger.info(f"开始刮削蓝光原盘:{path} ...")
|
||
scrape_path = path / path.name
|
||
self.scraper.gen_scraper_files(mediainfo=mediainfo,
|
||
file_path=scrape_path,
|
||
transfer_type=transfer_type,
|
||
metainfo=metainfo,
|
||
force_nfo=force_nfo,
|
||
force_img=force_img)
|
||
elif path.is_file():
|
||
# 单个文件
|
||
logger.info(f"开始刮削媒体库文件:{path} ...")
|
||
self.scraper.gen_scraper_files(mediainfo=mediainfo,
|
||
file_path=path,
|
||
transfer_type=transfer_type,
|
||
metainfo=metainfo,
|
||
force_nfo=force_nfo,
|
||
force_img=force_img)
|
||
else:
|
||
# 目录下的所有文件
|
||
logger.info(f"开始刮削目录:{path} ...")
|
||
for file in SystemUtils.list_files(path, settings.RMT_MEDIAEXT):
|
||
if not file:
|
||
continue
|
||
self.scraper.gen_scraper_files(mediainfo=mediainfo,
|
||
file_path=file,
|
||
transfer_type=transfer_type,
|
||
force_nfo=force_nfo,
|
||
force_img=force_img)
|
||
logger.info(f"{path} 刮削完成")
|
||
|
||
def metadata_nfo(self, meta: MetaBase, mediainfo: MediaInfo,
|
||
season: int = None, episode: int = None) -> Optional[str]:
|
||
"""
|
||
获取NFO文件内容文本
|
||
:param meta: 元数据
|
||
:param mediainfo: 媒体信息
|
||
:param season: 季号
|
||
:param episode: 集号
|
||
"""
|
||
if settings.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: int = None) -> Optional[dict]:
|
||
"""
|
||
获取图片名称和url
|
||
:param mediainfo: 媒体信息
|
||
:param season: 季号
|
||
"""
|
||
if settings.SCRAP_SOURCE != "themoviedb":
|
||
return None
|
||
return self.scraper.get_metadata_img(mediainfo=mediainfo, season=season)
|
||
|
||
def tmdb_discover(self, mtype: MediaType, sort_by: str, with_genres: str, with_original_language: str,
|
||
page: int = 1) -> Optional[List[MediaInfo]]:
|
||
"""
|
||
:param mtype: 媒体类型
|
||
:param sort_by: 排序方式
|
||
:param with_genres: 类型
|
||
:param with_original_language: 语言
|
||
: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,
|
||
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,
|
||
page=page)
|
||
else:
|
||
return []
|
||
if infos:
|
||
return [MediaInfo(tmdb_info=info) for info in infos]
|
||
return []
|
||
|
||
def tmdb_trending(self, page: int = 1) -> List[MediaInfo]:
|
||
"""
|
||
TMDB流行趋势
|
||
:param page: 第几页
|
||
:return: TMDB信息列表
|
||
"""
|
||
trending = self.tmdb.trending.all_week(page=page)
|
||
if trending:
|
||
return [MediaInfo(tmdb_info=info) for info in trending]
|
||
return []
|
||
|
||
def tmdb_seasons(self, tmdbid: int) -> List[schemas.TmdbSeason]:
|
||
"""
|
||
根据TMDBID查询themoviedb所有季信息
|
||
:param tmdbid: TMDBID
|
||
"""
|
||
tmdb_info = self.tmdb.get_info(tmdbid=tmdbid, mtype=MediaType.TV)
|
||
if not tmdb_info:
|
||
return []
|
||
return [schemas.TmdbSeason(**season)
|
||
for season in tmdb_info.get("seasons", []) if season.get("season_number")]
|
||
|
||
def tmdb_episodes(self, tmdbid: int, season: int) -> List[schemas.TmdbEpisode]:
|
||
"""
|
||
根据TMDBID查询某季的所有信信息
|
||
:param tmdbid: TMDBID
|
||
:param season: 季
|
||
"""
|
||
season_info = self.tmdb.get_tv_season_detail(tmdbid=tmdbid, season=season)
|
||
if not season_info or not season_info.get("episodes"):
|
||
return []
|
||
return [schemas.TmdbEpisode(**episode) for episode in season_info.get("episodes")]
|
||
|
||
def scheduler_job(self) -> None:
|
||
"""
|
||
定时任务,每10分钟调用一次
|
||
"""
|
||
self.cache.save()
|
||
|
||
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
|
||
"""
|
||
补充抓取媒体信息图片
|
||
:param mediainfo: 识别的媒体信息
|
||
:return: 更新后的媒体信息
|
||
"""
|
||
if settings.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
|
||
# 调用TMDB图片接口
|
||
if mediainfo.type == MediaType.MOVIE:
|
||
images = self.tmdb.get_movie_images(mediainfo.tmdb_id)
|
||
else:
|
||
images = self.tmdb.get_tv_images(mediainfo.tmdb_id)
|
||
if not images:
|
||
return mediainfo
|
||
if isinstance(images, list):
|
||
images = images[0]
|
||
# 背景图
|
||
if not mediainfo.backdrop_path:
|
||
backdrops = images.get("backdrops")
|
||
if backdrops:
|
||
backdrops = sorted(backdrops, key=lambda x: x.get("vote_average"), reverse=True)
|
||
mediainfo.backdrop_path = backdrops[0].get("file_path")
|
||
# 标志
|
||
if not mediainfo.logo_path:
|
||
logos = images.get("logos")
|
||
if logos:
|
||
logos = sorted(logos, key=lambda x: x.get("vote_average"), reverse=True)
|
||
mediainfo.logo_path = logos[0].get("file_path")
|
||
# 海报
|
||
if not mediainfo.poster_path:
|
||
posters = images.get("posters")
|
||
if posters:
|
||
posters = sorted(posters, key=lambda x: x.get("vote_average"), reverse=True)
|
||
mediainfo.poster_path = posters[0].get("file_path")
|
||
return mediainfo
|
||
|
||
def obtain_specific_image(self, mediaid: Union[str, int], mtype: MediaType,
|
||
image_type: MediaImageType, image_prefix: str = "w500",
|
||
season: int = None, episode: 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 f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/{image_prefix}{image_path}"
|
||
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: int = 1) -> List[schemas.MediaPerson]:
|
||
"""
|
||
根据TMDBID查询电影演职员表
|
||
:param tmdbid: TMDBID
|
||
:param page: 页码
|
||
"""
|
||
credit_infos = self.tmdb.get_movie_credits(tmdbid=tmdbid, page=page)
|
||
if credit_infos:
|
||
return [schemas.MediaPerson(source="themoviedb", **info) for info in credit_infos]
|
||
return []
|
||
|
||
def tmdb_tv_credits(self, tmdbid: int, page: int = 1) -> List[schemas.MediaPerson]:
|
||
"""
|
||
根据TMDBID查询电视剧演职员表
|
||
:param tmdbid: TMDBID
|
||
:param page: 页码
|
||
"""
|
||
credit_infos = self.tmdb.get_tv_credits(tmdbid=tmdbid, page=page)
|
||
if credit_infos:
|
||
return [schemas.MediaPerson(source="themoviedb", **info) for info in credit_infos]
|
||
return []
|
||
|
||
def tmdb_person_detail(self, person_id: int) -> schemas.MediaPerson:
|
||
"""
|
||
根据TMDBID查询人物详情
|
||
:param person_id: 人物ID
|
||
"""
|
||
detail = self.tmdb.get_person_detail(person_id=person_id)
|
||
if detail:
|
||
return schemas.MediaPerson(source="themoviedb", **detail)
|
||
return schemas.MediaPerson()
|
||
|
||
def tmdb_person_credits(self, person_id: int, page: 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 []
|
||
|
||
def clear_cache(self):
|
||
"""
|
||
清除缓存
|
||
"""
|
||
logger.info("开始清除TMDB缓存 ...")
|
||
self.tmdb.clear_cache()
|
||
self.cache.clear()
|
||
logger.info("TMDB缓存清除完成")
|