为TheMovieDb模块添加异步支持(part 1)

This commit is contained in:
jxxghp
2025-07-30 22:28:12 +08:00
parent 232759829e
commit 423c9af786
5 changed files with 350 additions and 2 deletions

View File

@@ -689,6 +689,99 @@ class TheMovieDbModule(_ModuleBase):
return [MediaInfo(tmdb_info=tmdbinfo) for tmdbinfo in infos] return [MediaInfo(tmdb_info=tmdbinfo) for tmdbinfo in infos]
return [] 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): def clear_cache(self):
""" """
清除缓存 清除缓存

View File

@@ -521,6 +521,7 @@ class TmdbApi:
raise APIRateLimitException("触发TheDbMovie网站限流获取媒体信息失败") raise APIRateLimitException("触发TheDbMovie网站限流获取媒体信息失败")
if res.status_code != 200: if res.status_code != 200:
return {} return {}
html = None
html_text = res.text html_text = res.text
if not html_text: if not html_text:
return {} return {}
@@ -560,13 +561,13 @@ class TmdbApi:
logger.info("%s TMDB网站返回数据过多%s" % (name, len(tmdb_links))) logger.info("%s TMDB网站返回数据过多%s" % (name, len(tmdb_links)))
else: else:
logger.info("%s TMDB网站未查询到媒体信息" % name) logger.info("%s TMDB网站未查询到媒体信息" % name)
return {}
except Exception as err: except Exception as err:
logger.error(f"从TheDbMovie网站查询出错{str(err)}") logger.error(f"从TheDbMovie网站查询出错{str(err)}")
return {} return {}
finally: finally:
if html is not None: if html is not None:
del html del html
return {}
def get_info(self, def get_info(self,
mtype: MediaType, mtype: MediaType,
@@ -639,6 +640,7 @@ class TmdbApi:
return None return None
# dict[地区:分级] # dict[地区:分级]
ratings = {} ratings = {}
results = []
if results := (tmdb_info.get("release_dates") or {}).get("results"): if results := (tmdb_info.get("release_dates") or {}).get("results"):
""" """
[ [
@@ -1424,6 +1426,85 @@ class TmdbApi:
""" """
self.tmdb.cache_clear() 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): def close(self):
""" """
关闭连接 关闭连接

View File

@@ -32,3 +32,24 @@ class Discover(TMDb):
:return: :return:
""" """
return self._request_obj(self._urls["tv"], urlencode(params_tuple), key="results", call_cached=False) 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)

View File

@@ -142,3 +142,71 @@ class Search(TMDb):
params=params, params=params,
key="results" 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 += "&region=%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 += "&region=%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"
)

View File

@@ -9,7 +9,7 @@ import requests.exceptions
from app.core.cache import cached from app.core.cache import cached
from app.core.config import settings from app.core.config import settings
from app.utils.http import RequestUtils from app.utils.http import RequestUtils, AsyncRequestUtils
from .exceptions import TMDbException from .exceptions import TMDbException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -17,6 +17,7 @@ logger = logging.getLogger(__name__)
class TMDb(object): class TMDb(object):
_req = None _req = None
_async_req = None
_session = None _session = None
def __init__(self, obj_cached=True, session=None, language=None): def __init__(self, obj_cached=True, session=None, language=None):
@@ -37,6 +38,8 @@ class TMDb(object):
else: else:
self._session = requests.Session() self._session = requests.Session()
self._req = RequestUtils(session=self._session, proxies=self.proxies) self._req = RequestUtils(session=self._session, proxies=self.proxies)
# 初始化异步请求客户端
self._async_req = AsyncRequestUtils(proxies=self.proxies)
self._remaining = 40 self._remaining = 40
self._reset = None self._reset = None
self._timeout = 15 self._timeout = 15
@@ -132,6 +135,14 @@ class TMDb(object):
""" """
return self.request(method, url, data, json) 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): def request(self, method, url, data, json):
if method == "GET": if method == "GET":
req = self._req.get_res(url, params=data, json=json) req = self._req.get_res(url, params=data, json=json)
@@ -141,6 +152,15 @@ class TMDb(object):
raise TMDbException("无法连接TheMovieDb请检查网络连接") raise TMDbException("无法连接TheMovieDb请检查网络连接")
return req 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): def cache_clear(self):
return self.cached_request.cache_clear() return self.cached_request.cache_clear()
@@ -209,6 +229,71 @@ class TMDb(object):
return json_data.get(key) return json_data.get(key)
return json_data 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): def close(self):
if self._session: if self._session:
self._session.close() self._session.close()