From 423c9af786e56df89ef0f084fce6dc6d4ba28efc Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 30 Jul 2025 22:28:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BATheMovieDb=E6=A8=A1=E5=9D=97=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=BC=82=E6=AD=A5=E6=94=AF=E6=8C=81=EF=BC=88part=201?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modules/themoviedb/__init__.py | 93 +++++++++++++++++++ app/modules/themoviedb/tmdbapi.py | 83 ++++++++++++++++- .../themoviedb/tmdbv3api/objs/discover.py | 21 +++++ .../themoviedb/tmdbv3api/objs/search.py | 68 ++++++++++++++ app/modules/themoviedb/tmdbv3api/tmdb.py | 87 ++++++++++++++++- 5 files changed, 350 insertions(+), 2 deletions(-) diff --git a/app/modules/themoviedb/__init__.py b/app/modules/themoviedb/__init__.py index 7db0c454..8634a33b 100644 --- a/app/modules/themoviedb/__init__.py +++ b/app/modules/themoviedb/__init__.py @@ -689,6 +689,99 @@ class TheMovieDbModule(_ModuleBase): return [MediaInfo(tmdb_info=tmdbinfo) for tmdbinfo in infos] return [] + # 异步方法 + async def async_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 = 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) + # 将搜索词中的季写入标题中 + 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 [] + + 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) -> 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 + }) + 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 + }) + else: + return [] + if infos: + return [MediaInfo(tmdb_info=info) for info in infos] + return [] + def clear_cache(self): """ 清除缓存 diff --git a/app/modules/themoviedb/tmdbapi.py b/app/modules/themoviedb/tmdbapi.py index 56ef2dc1..23ebe07f 100644 --- a/app/modules/themoviedb/tmdbapi.py +++ b/app/modules/themoviedb/tmdbapi.py @@ -521,6 +521,7 @@ class TmdbApi: raise APIRateLimitException("触发TheDbMovie网站限流,获取媒体信息失败") if res.status_code != 200: return {} + html = None html_text = res.text if not html_text: return {} @@ -560,13 +561,13 @@ class TmdbApi: logger.info("%s TMDB网站返回数据过多:%s" % (name, len(tmdb_links))) else: logger.info("%s TMDB网站未查询到媒体信息!" % name) + return {} except Exception as err: logger.error(f"从TheDbMovie网站查询出错:{str(err)}") return {} finally: if html is not None: del html - return {} def get_info(self, mtype: MediaType, @@ -639,6 +640,7 @@ class TmdbApi: return None # dict[地区:分级] ratings = {} + results = [] if results := (tmdb_info.get("release_dates") or {}).get("results"): """ [ @@ -1424,6 +1426,85 @@ class TmdbApi: """ self.tmdb.cache_clear() + # 异步方法 + async def async_search_multiis(self, title: str) -> List[dict]: + """ + 同时查询模糊匹配的电影、电视剧TMDB信息(异步版本) + """ + if not title: + return [] + ret_infos = [] + multis = await self.search.async_multi(term=title) or [] + for multi in multis: + if multi.get("media_type") in ["movie", "tv"]: + multi['media_type'] = MediaType.MOVIE if multi.get("media_type") == "movie" else MediaType.TV + ret_infos.append(multi) + return ret_infos + + async def async_search_movies(self, title: str, year: str) -> List[dict]: + """ + 查询模糊匹配的所有电影TMDB信息(异步版本) + """ + if not title: + return [] + ret_infos = [] + if year: + movies = await self.search.async_movies(term=title, year=year) or [] + else: + movies = await self.search.async_movies(term=title) or [] + for movie in movies: + if title in movie.get("title"): + movie['media_type'] = MediaType.MOVIE + ret_infos.append(movie) + return ret_infos + + async def async_search_tvs(self, title: str, year: str) -> List[dict]: + """ + 查询模糊匹配的所有电视剧TMDB信息(异步版本) + """ + if not title: + return [] + ret_infos = [] + if year: + tvs = await self.search.async_tv_shows(term=title, release_year=year) or [] + else: + tvs = await self.search.async_tv_shows(term=title) or [] + for tv in tvs: + if title in tv.get("name"): + tv['media_type'] = MediaType.TV + ret_infos.append(tv) + return ret_infos + + async def async_discover_movies(self, params: dict) -> List[dict]: + """ + 发现电影(异步版本) + """ + if not params: + return [] + try: + items = await self.discover.async_discover_movies(params_tuple=tuple(params.items())) or [] + for item in items: + item['media_type'] = MediaType.MOVIE + return items + except Exception as e: + logger.error(f"获取电影发现失败:{str(e)}") + return [] + + async def async_discover_tvs(self, params: dict) -> List[dict]: + """ + 发现电视剧(异步版本) + """ + if not params: + return [] + try: + items = await self.discover.async_discover_tv_shows(params_tuple=tuple(params.items())) or [] + for item in items: + item['media_type'] = MediaType.TV + return items + except Exception as e: + logger.error(f"获取电视剧发现失败:{str(e)}") + return [] + def close(self): """ 关闭连接 diff --git a/app/modules/themoviedb/tmdbv3api/objs/discover.py b/app/modules/themoviedb/tmdbv3api/objs/discover.py index 70f1ff41..4425b749 100644 --- a/app/modules/themoviedb/tmdbv3api/objs/discover.py +++ b/app/modules/themoviedb/tmdbv3api/objs/discover.py @@ -32,3 +32,24 @@ class Discover(TMDb): :return: """ return self._request_obj(self._urls["tv"], urlencode(params_tuple), key="results", call_cached=False) + + @cached(maxsize=1, ttl=43200) + async def async_discover_movies(self, params_tuple): + """ + Discover movies by different types of data like average rating, number of votes, genres and certifications.(异步版本) + :param params_tuple: dict + :return: + """ + params = dict(params_tuple) + return await self._async_request_obj(self._urls["movies"], urlencode(params), key="results", call_cached=False) + + @cached(maxsize=1, ttl=43200) + async def async_discover_tv_shows(self, params_tuple): + """ + Discover TV shows by different types of data like average rating, number of votes, genres, + the network they aired on and air dates.(异步版本) + :param params_tuple: dict + :return: + """ + return await self._async_request_obj(self._urls["tv"], urlencode(params_tuple), key="results", + call_cached=False) diff --git a/app/modules/themoviedb/tmdbv3api/objs/search.py b/app/modules/themoviedb/tmdbv3api/objs/search.py index 78912cb9..e03eb6d8 100644 --- a/app/modules/themoviedb/tmdbv3api/objs/search.py +++ b/app/modules/themoviedb/tmdbv3api/objs/search.py @@ -142,3 +142,71 @@ class Search(TMDb): params=params, key="results" ) + + async def async_multi(self, term, adult=None, region=None, page=1): + """ + Search multiple models in a single request.(异步版本) + Multi search currently supports searching for movies, tv shows and people in a single request. + :param term: str + :param adult: bool + :param region: str + :param page: int + :return: + """ + params = "query=%s&page=%s" % (quote(term), page) + if adult is not None: + params += "&include_adult=%s" % "true" if adult else "false" + if region is not None: + params += "®ion=%s" % quote(region) + return await self._async_request_obj( + self._urls["multi"], + params=params, + key="results" + ) + + async def async_movies(self, term, adult=None, region=None, year=None, release_year=None, page=1): + """ + Search for movies.(异步版本) + :param term: str + :param adult: bool + :param region: str + :param year: int + :param release_year: int + :param page: int + :return: + """ + params = "query=%s&page=%s" % (quote(term), page) + if adult is not None: + params += "&include_adult=%s" % "true" if adult else "false" + if region is not None: + params += "®ion=%s" % quote(region) + if year is not None: + params += "&year=%s" % year + if release_year is not None: + params += "&primary_release_year=%s" % release_year + + return await self._async_request_obj( + self._urls["movies"], + params=params, + key="results" + ) + + async def async_tv_shows(self, term, adult=None, release_year=None, page=1): + """ + Search for a TV show.(异步版本) + :param term: str + :param adult: bool + :param release_year: int + :param page: int + :return: + """ + params = "query=%s&page=%s" % (quote(term), page) + if adult is not None: + params += "&include_adult=%s" % "true" if adult else "false" + if release_year is not None: + params += "&first_air_date_year=%s" % release_year + return await self._async_request_obj( + self._urls["tv_shows"], + params=params, + key="results" + ) diff --git a/app/modules/themoviedb/tmdbv3api/tmdb.py b/app/modules/themoviedb/tmdbv3api/tmdb.py index 614dd6f9..c688511e 100644 --- a/app/modules/themoviedb/tmdbv3api/tmdb.py +++ b/app/modules/themoviedb/tmdbv3api/tmdb.py @@ -9,7 +9,7 @@ import requests.exceptions from app.core.cache import cached from app.core.config import settings -from app.utils.http import RequestUtils +from app.utils.http import RequestUtils, AsyncRequestUtils from .exceptions import TMDbException logger = logging.getLogger(__name__) @@ -17,6 +17,7 @@ logger = logging.getLogger(__name__) class TMDb(object): _req = None + _async_req = None _session = None def __init__(self, obj_cached=True, session=None, language=None): @@ -37,6 +38,8 @@ class TMDb(object): else: self._session = requests.Session() self._req = RequestUtils(session=self._session, proxies=self.proxies) + # 初始化异步请求客户端 + self._async_req = AsyncRequestUtils(proxies=self.proxies) self._remaining = 40 self._reset = None self._timeout = 15 @@ -132,6 +135,14 @@ class TMDb(object): """ return self.request(method, url, data, json) + @cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta) + async def async_cached_request(self, method, url, data, json, + _ts=datetime.strftime(datetime.now(), '%Y%m%d')): + """ + 缓存请求(异步版本) + """ + return await self.async_request(method, url, data, json) + def request(self, method, url, data, json): if method == "GET": req = self._req.get_res(url, params=data, json=json) @@ -141,6 +152,15 @@ class TMDb(object): raise TMDbException("无法连接TheMovieDb,请检查网络连接!") return req + async def async_request(self, method, url, data, json): + if method == "GET": + req = await self._async_req.get_res(url, params=data, json=json) + else: + req = await self._async_req.post_res(url, data=data, json=json) + if req is None: + raise TMDbException("无法连接TheMovieDb,请检查网络连接!") + return req + def cache_clear(self): return self.cached_request.cache_clear() @@ -209,6 +229,71 @@ class TMDb(object): return json_data.get(key) return json_data + async def _async_request_obj(self, action, params="", call_cached=True, + method="GET", data=None, json=None, key=None): + if self.api_key is None or self.api_key == "": + raise TMDbException("TheMovieDb API Key 未设置!") + + url = "https://%s/3%s?api_key=%s&%s&language=%s" % ( + self.domain, + action, + self.api_key, + params, + self.language, + ) + + if self.cache and self.obj_cached and call_cached and method != "POST": + req = await self.async_cached_request(method, url, data, json) + else: + req = await self.async_request(method, url, data, json) + + if req is None: + return None + + headers = req.headers + + if "X-RateLimit-Remaining" in headers: + self._remaining = int(headers["X-RateLimit-Remaining"]) + + if "X-RateLimit-Reset" in headers: + self._reset = int(headers["X-RateLimit-Reset"]) + + if self._remaining < 1: + current_time = int(time.time()) + sleep_time = self._reset - current_time + + if self.wait_on_rate_limit: + logger.warning("达到请求频率限制,休眠:%d 秒..." % sleep_time) + time.sleep(abs(sleep_time)) + return await self._async_request_obj(action, params, call_cached, method, data, json, key) + else: + raise TMDbException("达到请求频率限制,将在 %d 秒后重试..." % sleep_time) + + json_data = req.json() + + if "page" in json_data: + self._page = json_data["page"] + + if "total_results" in json_data: + self._total_results = json_data["total_results"] + + if "total_pages" in json_data: + self._total_pages = json_data["total_pages"] + + if self.debug: + logger.info(json_data) + logger.info(self.async_cached_request.cache_info()) + + if "errors" in json_data: + raise TMDbException(json_data["errors"]) + + if "success" in json_data and json_data["success"] is False: + raise TMDbException(json_data["status_message"]) + + if key: + return json_data.get(key) + return json_data + def close(self): if self._session: self._session.close()