为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 []
# 异步方法
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):
"""
清除缓存

View File

@@ -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):
"""
关闭连接

View File

@@ -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)

View File

@@ -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 += "&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.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()