mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-09 01:16:50 +08:00
fix(dashboard): distinguish empty media results (#6187)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from typing import Any, List, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app import schemas
|
||||
@@ -21,6 +21,18 @@ from app.utils.media import build_media_key, resolve_media_identity
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _require_mediaserver_result(result: Optional[List[Any]]) -> List[Any]:
|
||||
"""
|
||||
保留媒体服务器成功空列表,并把提供方失败转换为明确的网关错误。
|
||||
"""
|
||||
if result is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="媒体服务器请求失败",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/play/{itemid:path}", summary="在线播放")
|
||||
def play_item(
|
||||
itemid: str, _: schemas.TokenPayload = Depends(verify_token)
|
||||
@@ -153,11 +165,12 @@ def latest(
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
return (
|
||||
return _require_mediaserver_result(
|
||||
MediaServerChain().latest(
|
||||
server=server, count=count, username=userinfo.username
|
||||
server=server,
|
||||
count=count,
|
||||
username=userinfo.username,
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
|
||||
@@ -172,11 +185,12 @@ def playing(
|
||||
"""
|
||||
获取媒体服务器正在播放条目
|
||||
"""
|
||||
return (
|
||||
return _require_mediaserver_result(
|
||||
MediaServerChain().playing(
|
||||
server=server, count=count, username=userinfo.username
|
||||
server=server,
|
||||
count=count,
|
||||
username=userinfo.username,
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
|
||||
@@ -191,11 +205,12 @@ def library(
|
||||
"""
|
||||
获取媒体服务器媒体库列表
|
||||
"""
|
||||
return (
|
||||
return _require_mediaserver_result(
|
||||
MediaServerChain().librarys(
|
||||
server=server, username=userinfo.username, hidden=hidden
|
||||
server=server,
|
||||
username=userinfo.username,
|
||||
hidden=hidden,
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
from typing import Any, List, Optional
|
||||
from typing import Any, Awaitable, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from app import schemas
|
||||
from app.chain.recommend import RecommendChain
|
||||
from app.core.event import eventmanager
|
||||
from app.core.security import verify_token
|
||||
from app.modules.themoviedb.tmdbv3api.exceptions import TMDbException
|
||||
from app.schemas import RecommendSourceEventData
|
||||
from app.schemas.types import ChainEventType
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def _require_tmdb_result(operation: Awaitable[List[Any]]) -> List[Any]:
|
||||
"""保留 TMDB 成功空列表,并把上游请求异常转换为明确的网关错误。"""
|
||||
try:
|
||||
return await operation
|
||||
except TMDbException as error:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="TMDB请求失败",
|
||||
) from error
|
||||
|
||||
|
||||
@router.get(
|
||||
"/source",
|
||||
summary="获取推荐数据源",
|
||||
@@ -204,16 +216,19 @@ async def tmdb_movies(
|
||||
"""
|
||||
浏览TMDB电影信息
|
||||
"""
|
||||
return await RecommendChain().async_tmdb_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=vote_average,
|
||||
vote_count=vote_count,
|
||||
release_date=release_date,
|
||||
page=page,
|
||||
return await _require_tmdb_result(
|
||||
RecommendChain().async_tmdb_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=vote_average,
|
||||
vote_count=vote_count,
|
||||
release_date=release_date,
|
||||
page=page,
|
||||
raise_exception=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -233,16 +248,19 @@ async def tmdb_tvs(
|
||||
"""
|
||||
浏览TMDB剧集信息
|
||||
"""
|
||||
return await RecommendChain().async_tmdb_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=vote_average,
|
||||
vote_count=vote_count,
|
||||
release_date=release_date,
|
||||
page=page,
|
||||
return await _require_tmdb_result(
|
||||
RecommendChain().async_tmdb_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=vote_average,
|
||||
vote_count=vote_count,
|
||||
release_date=release_date,
|
||||
page=page,
|
||||
raise_exception=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -255,4 +273,6 @@ async def tmdb_trending(
|
||||
"""
|
||||
TMDB流行趋势
|
||||
"""
|
||||
return await RecommendChain().async_tmdb_trending(page=page)
|
||||
return await _require_tmdb_result(
|
||||
RecommendChain().async_tmdb_trending(page=page, raise_exception=True)
|
||||
)
|
||||
|
||||
+15
-11
@@ -27,11 +27,13 @@ class MediaServerChain(ChainBase):
|
||||
|
||||
def _sign_library_images(
|
||||
self, libraries: Optional[List[MediaServerLibrary]]
|
||||
) -> List[MediaServerLibrary]:
|
||||
) -> Optional[List[MediaServerLibrary]]:
|
||||
"""
|
||||
给媒体库列表中的封面和封面组添加代理签名。
|
||||
给媒体库列表中的封面和封面组添加代理签名,并保留提供方失败状态。
|
||||
"""
|
||||
for library in libraries or []:
|
||||
if libraries is None:
|
||||
return None
|
||||
for library in libraries:
|
||||
if library.image:
|
||||
library.image = self._sign_image_url(library.image)
|
||||
if library.image_list:
|
||||
@@ -40,21 +42,23 @@ class MediaServerChain(ChainBase):
|
||||
for image in library.image_list
|
||||
if image
|
||||
]
|
||||
return libraries or []
|
||||
return libraries
|
||||
|
||||
def _sign_play_item_images(
|
||||
self, items: Optional[List[MediaServerPlayItem]]
|
||||
) -> List[MediaServerPlayItem]:
|
||||
) -> Optional[List[MediaServerPlayItem]]:
|
||||
"""
|
||||
给媒体服务器播放条目中的图片 URL 添加代理签名。
|
||||
给媒体服务器播放条目中的图片 URL 添加代理签名,并保留提供方失败状态。
|
||||
"""
|
||||
for item in items or []:
|
||||
if items is None:
|
||||
return None
|
||||
for item in items:
|
||||
if item.image:
|
||||
item.image = self._sign_image_url(item.image)
|
||||
return items or []
|
||||
return items
|
||||
|
||||
def librarys(self, server: str, username: Optional[str] = None,
|
||||
hidden: bool = False) -> List[MediaServerLibrary]:
|
||||
hidden: bool = False) -> Optional[List[MediaServerLibrary]]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库
|
||||
"""
|
||||
@@ -151,7 +155,7 @@ class MediaServerChain(ChainBase):
|
||||
return self.run_module("mediaserver_tv_episodes", server=server, item_id=item_id)
|
||||
|
||||
def playing(self, server: str, count: Optional[int] = 20,
|
||||
username: Optional[str] = None) -> List[MediaServerPlayItem]:
|
||||
username: Optional[str] = None) -> Optional[List[MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
@@ -165,7 +169,7 @@ class MediaServerChain(ChainBase):
|
||||
)
|
||||
|
||||
def latest(self, server: str, count: Optional[int] = 20,
|
||||
username: Optional[str] = None) -> List[MediaServerPlayItem]:
|
||||
username: Optional[str] = None) -> Optional[List[MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
|
||||
+16
-6
@@ -313,7 +313,8 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
vote_average: Optional[float] = 0.0,
|
||||
vote_count: Optional[int] = 0,
|
||||
release_date: Optional[str] = "",
|
||||
page: Optional[int] = 1) -> List[dict]:
|
||||
page: Optional[int] = 1,
|
||||
raise_exception: bool = False) -> List[dict]:
|
||||
"""
|
||||
异步TMDB热门电影
|
||||
"""
|
||||
@@ -326,7 +327,8 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
vote_average=vote_average,
|
||||
vote_count=vote_count,
|
||||
release_date=release_date,
|
||||
page=page)
|
||||
page=page,
|
||||
raise_exception=raise_exception)
|
||||
return [movie.to_dict() for movie in movies] if movies else []
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@@ -339,7 +341,8 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
vote_average: Optional[float] = 0.0,
|
||||
vote_count: Optional[int] = 0,
|
||||
release_date: Optional[str] = "",
|
||||
page: Optional[int] = 1) -> List[dict]:
|
||||
page: Optional[int] = 1,
|
||||
raise_exception: bool = False) -> List[dict]:
|
||||
"""
|
||||
异步TMDB热门电视剧
|
||||
"""
|
||||
@@ -352,16 +355,23 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
vote_average=vote_average,
|
||||
vote_count=vote_count,
|
||||
release_date=release_date,
|
||||
page=page)
|
||||
page=page,
|
||||
raise_exception=raise_exception)
|
||||
return [tv.to_dict() for tv in tvs] if tvs else []
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
async def async_tmdb_trending(self, page: Optional[int] = 1) -> List[dict]:
|
||||
async def async_tmdb_trending(
|
||||
self, page: Optional[int] = 1, raise_exception: bool = False
|
||||
) -> List[dict]:
|
||||
"""
|
||||
异步TMDB流行趋势
|
||||
"""
|
||||
infos = await TmdbChain().async_run_module("async_tmdb_trending", page=page)
|
||||
infos = await TmdbChain().async_run_module(
|
||||
"async_tmdb_trending",
|
||||
page=page,
|
||||
raise_exception=raise_exception,
|
||||
)
|
||||
return [info.to_dict() for info in infos] if infos else []
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"messages": {
|
||||
"模块不支持测试": "Module does not support testing",
|
||||
"网络请求失败": "Network request failed",
|
||||
"TMDB请求失败": "TMDB request failed",
|
||||
"媒体服务器请求失败": "Media server request failed",
|
||||
"附件保存失败": "Failed to save attachment",
|
||||
"该选择已失效,请重新发起选择": "This selection has expired. Please start the selection again",
|
||||
"会话不存在或无权访问": "The conversation does not exist or you do not have access",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"messages": {
|
||||
"模块不支持测试": "模块不支持测试",
|
||||
"网络请求失败": "网络请求失败",
|
||||
"TMDB请求失败": "TMDB请求失败",
|
||||
"媒体服务器请求失败": "媒体服务器请求失败",
|
||||
"豆瓣网络连接失败": "豆瓣网络连接失败",
|
||||
"Bangumi网络连接失败": "Bangumi网络连接失败",
|
||||
"fanart网络连接失败": "fanart网络连接失败",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"messages": {
|
||||
"模块不支持测试": "模組不支援測試",
|
||||
"网络请求失败": "網路請求失敗",
|
||||
"TMDB请求失败": "TMDB 請求失敗",
|
||||
"媒体服务器请求失败": "媒體伺服器請求失敗",
|
||||
"附件保存失败": "附件儲存失敗",
|
||||
"该选择已失效,请重新发起选择": "此選擇已失效,請重新發起選擇",
|
||||
"会话不存在或无权访问": "會話不存在或無權存取",
|
||||
|
||||
@@ -282,13 +282,13 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
||||
) for season, episodes in seasoninfo.items()]
|
||||
|
||||
def mediaserver_playing(self, server: str, count: Optional[int] = 20,
|
||||
username: Optional[str] = None) -> List[schemas.MediaServerPlayItem]:
|
||||
username: Optional[str] = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
server_obj: Emby = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return None
|
||||
return server_obj.get_resume(num=count, username=username)
|
||||
|
||||
def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]:
|
||||
@@ -316,13 +316,13 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
||||
return server_obj.get_season_episode_ids(str(item_id), season)
|
||||
|
||||
def mediaserver_latest(self, server: Optional[str] = None, count: Optional[int] = 20,
|
||||
username: Optional[str] = None) -> List[schemas.MediaServerPlayItem]:
|
||||
username: Optional[str] = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
server_obj: Emby = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return None
|
||||
return server_obj.get_latest(num=count, username=username)
|
||||
|
||||
def mediaserver_latest_images(self,
|
||||
@@ -345,8 +345,7 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
||||
return []
|
||||
|
||||
links = []
|
||||
items: List[schemas.MediaServerPlayItem] = self.mediaserver_latest(server=server, count=count,
|
||||
username=username)
|
||||
items = self.mediaserver_latest(server=server, count=count, username=username) or []
|
||||
for item in items:
|
||||
if item.BackdropImageTags:
|
||||
image_url = server_obj.get_backdrop_url(item_id=item.id,
|
||||
|
||||
+24
-11
@@ -118,38 +118,47 @@ class Emby:
|
||||
logger.error(f"连接Library/VirtualFolders/Query 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def __get_emby_librarys(self, username: Optional[str] = None) -> List[dict]:
|
||||
def __get_emby_librarys(self, username: Optional[str] = None) -> Optional[List[dict]]:
|
||||
"""
|
||||
获取Emby媒体库列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
return None
|
||||
if username:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
if not user:
|
||||
return None
|
||||
url = f"{self._host}emby/Users/{user}/Views"
|
||||
params = {"api_key": self._apikey}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
if res:
|
||||
return res.json().get("Items")
|
||||
items = res.json().get("Items")
|
||||
return items if isinstance(items, list) else None
|
||||
else:
|
||||
logger.error(f"User/Views 未获取到返回数据")
|
||||
return []
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"连接User/Views 出错:" + str(e))
|
||||
return []
|
||||
return None
|
||||
|
||||
def get_librarys(self, username: Optional[str] = None, hidden: Optional[bool] = False) -> List[
|
||||
schemas.MediaServerLibrary]:
|
||||
def get_librarys(
|
||||
self,
|
||||
username: Optional[str] = None,
|
||||
hidden: Optional[bool] = False,
|
||||
) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
return None
|
||||
source_libraries = self.__get_emby_librarys(username)
|
||||
if source_libraries is None:
|
||||
return None
|
||||
libraries = []
|
||||
for library in self.__get_emby_librarys(username) or []:
|
||||
for library in source_libraries:
|
||||
if hidden and self._sync_libraries and "all" not in self._sync_libraries \
|
||||
and library.get("Id") not in self._sync_libraries:
|
||||
continue
|
||||
@@ -1206,6 +1215,8 @@ class Emby:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
if not user:
|
||||
return None
|
||||
url = f"{self._host}Users/{user}/Items/Resume"
|
||||
params = {
|
||||
"Limit": 100,
|
||||
@@ -1266,7 +1277,7 @@ class Emby:
|
||||
logger.error(f"Users/Items/Resume 未获取到返回数据")
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items/Resume出错:" + str(e))
|
||||
return []
|
||||
return None
|
||||
|
||||
def get_latest(self, num: Optional[int] = 20, username: Optional[str] = None) -> Optional[
|
||||
List[schemas.MediaServerPlayItem]]:
|
||||
@@ -1279,6 +1290,8 @@ class Emby:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
if not user:
|
||||
return None
|
||||
url = f"{self._host}Users/{user}/Items/Latest"
|
||||
params = {
|
||||
"Limit": 100,
|
||||
@@ -1323,7 +1336,7 @@ class Emby:
|
||||
logger.error(f"Users/Items/Latest 未获取到返回数据")
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items/Latest出错:" + str(e))
|
||||
return []
|
||||
return None
|
||||
|
||||
def get_user_library_folders(self):
|
||||
"""
|
||||
|
||||
@@ -281,13 +281,14 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
||||
) for season, episodes in seasoninfo.items()]
|
||||
|
||||
def mediaserver_playing(self, server: str,
|
||||
count: Optional[int] = 20, username: Optional[str] = None) -> List[schemas.MediaServerPlayItem]:
|
||||
count: Optional[int] = 20,
|
||||
username: Optional[str] = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
server_obj: Jellyfin = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return None
|
||||
return server_obj.get_resume(num=count, username=username)
|
||||
|
||||
def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]:
|
||||
@@ -315,13 +316,13 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
||||
return server_obj.get_season_episode_ids(str(item_id), season)
|
||||
|
||||
def mediaserver_latest(self, server: Optional[str] = None, count: Optional[int] = 20,
|
||||
username: Optional[str] = None) -> List[schemas.MediaServerPlayItem]:
|
||||
username: Optional[str] = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
server_obj: Jellyfin = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return None
|
||||
return server_obj.get_latest(num=count, username=username)
|
||||
|
||||
def mediaserver_latest_images(self,
|
||||
@@ -344,8 +345,7 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
||||
return []
|
||||
|
||||
links = []
|
||||
items: List[schemas.MediaServerPlayItem] = self.mediaserver_latest(server=server, count=count,
|
||||
username=username)
|
||||
items = self.mediaserver_latest(server=server, count=count, username=username) or []
|
||||
for item in items:
|
||||
if item.BackdropImageTags:
|
||||
image_url = server_obj.get_backdrop_url(item_id=item.id,
|
||||
|
||||
@@ -114,42 +114,50 @@ class Jellyfin:
|
||||
logger.error(f"连接Library/VirtualFolders 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def __get_jellyfin_librarys(self, username: Optional[str] = None) -> List[dict]:
|
||||
def __get_jellyfin_librarys(self, username: Optional[str] = None) -> Optional[List[dict]]:
|
||||
"""
|
||||
获取Jellyfin媒体库的信息
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
return None
|
||||
if username:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
if not user:
|
||||
return []
|
||||
return None
|
||||
# 使用标准库路径拼接结合统一 URL 规整,避免 host 尾部斜杠缺失导致的寻址偏移。
|
||||
url = UrlUtils.combine_url(self._host, posixpath.join("Users", str(user), "Views"))
|
||||
if not url:
|
||||
return []
|
||||
return None
|
||||
params = {"api_key": self._apikey}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
if res:
|
||||
return res.json().get("Items")
|
||||
items = res.json().get("Items")
|
||||
return items if isinstance(items, list) else None
|
||||
else:
|
||||
logger.error(f"Users/Views 未获取到返回数据")
|
||||
return []
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Views 出错:" + str(e))
|
||||
return []
|
||||
return None
|
||||
|
||||
def get_librarys(self, username: Optional[str] = None, hidden: Optional[bool] = False) -> List[schemas.MediaServerLibrary]:
|
||||
def get_librarys(
|
||||
self,
|
||||
username: Optional[str] = None,
|
||||
hidden: Optional[bool] = False,
|
||||
) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
return None
|
||||
source_libraries = self.__get_jellyfin_librarys(username)
|
||||
if source_libraries is None:
|
||||
return None
|
||||
libraries = []
|
||||
for library in self.__get_jellyfin_librarys(username) or []:
|
||||
for library in source_libraries:
|
||||
if hidden and self._sync_libraries and "all" not in self._sync_libraries \
|
||||
and library.get("Id") not in self._sync_libraries:
|
||||
continue
|
||||
@@ -1027,6 +1035,8 @@ class Jellyfin:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
if not user:
|
||||
return None
|
||||
|
||||
url = f"{self._host}Users/{user}/Items/Resume"
|
||||
params = {
|
||||
@@ -1083,7 +1093,7 @@ class Jellyfin:
|
||||
logger.error(f"Users/Items/Resume 未获取到返回数据")
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items/Resume出错:" + str(e))
|
||||
return []
|
||||
return None
|
||||
|
||||
def get_latest(self, num=20, username: Optional[str] = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
@@ -1095,6 +1105,8 @@ class Jellyfin:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
if not user:
|
||||
return None
|
||||
url = f"{self._host}Users/{user}/Items/Latest"
|
||||
params = {
|
||||
"Limit": 100,
|
||||
@@ -1136,7 +1148,7 @@ class Jellyfin:
|
||||
logger.error(f"Users/Items/Latest 未获取到返回数据")
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items/Latest出错:" + str(e))
|
||||
return []
|
||||
return None
|
||||
|
||||
def get_user_library_folders(self):
|
||||
"""
|
||||
|
||||
@@ -293,23 +293,23 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
|
||||
) for season, episodes in seasoninfo.items()]
|
||||
|
||||
def mediaserver_playing(self, server: str, count: Optional[int] = 20,
|
||||
**kwargs) -> List[schemas.MediaServerPlayItem]:
|
||||
**kwargs) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
server_obj: Plex = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return None
|
||||
return server_obj.get_resume(num=count)
|
||||
|
||||
def mediaserver_latest(self, server: Optional[str] = None, count: Optional[int] = 20,
|
||||
**kwargs) -> List[schemas.MediaServerPlayItem]:
|
||||
**kwargs) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
server_obj: Plex = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return None
|
||||
return server_obj.get_latest(num=count)
|
||||
|
||||
def mediaserver_latest_images(self,
|
||||
@@ -331,8 +331,7 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
|
||||
return []
|
||||
|
||||
links = []
|
||||
items: List[schemas.MediaServerPlayItem] = self.mediaserver_latest(server=server, count=count,
|
||||
username=username)
|
||||
items = self.mediaserver_latest(server=server, count=count, username=username) or []
|
||||
for item in items:
|
||||
link = server_obj.get_remote_image_by_id(item_id=item.id,
|
||||
image_type="Backdrop",
|
||||
|
||||
@@ -122,17 +122,17 @@ class Plex:
|
||||
return [f"{self._host.rstrip('/') + url}?X-Plex-Token={self._token}" for url in
|
||||
list(poster_urls.keys())[:total_size]]
|
||||
|
||||
def get_librarys(self, hidden: Optional[bool] = False) -> List[schemas.MediaServerLibrary]:
|
||||
def get_librarys(self, hidden: Optional[bool] = False) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not self._plex:
|
||||
return []
|
||||
return None
|
||||
try:
|
||||
self._libraries = self._plex.library.sections()
|
||||
except Exception as err:
|
||||
logger.error(f"获取媒体服务器所有媒体库列表出错:{str(err)}")
|
||||
return []
|
||||
return None
|
||||
libraries = []
|
||||
for library in self._libraries:
|
||||
if hidden and self._sync_libraries and "all" not in self._sync_libraries \
|
||||
@@ -171,7 +171,7 @@ class Plex:
|
||||
sections = self._plex.library.sections()
|
||||
movie_count = tv_count = episode_count = 0
|
||||
# 媒体库白名单
|
||||
allow_library = [str(lib.id) for lib in self.get_librarys(hidden=True)]
|
||||
allow_library = [str(lib.id) for lib in self.get_librarys(hidden=True) or []]
|
||||
for sec in sections:
|
||||
if str(sec.key) not in allow_library:
|
||||
continue
|
||||
@@ -832,9 +832,12 @@ class Plex:
|
||||
获取继续观看的媒体
|
||||
"""
|
||||
if not self._plex:
|
||||
return []
|
||||
return None
|
||||
# 媒体库白名单
|
||||
allow_library = ",".join(map(str, (lib.id for lib in self.get_librarys(hidden=True))))
|
||||
libraries = self.get_librarys(hidden=True)
|
||||
if libraries is None:
|
||||
return None
|
||||
allow_library = ",".join(map(str, (lib.id for lib in libraries)))
|
||||
params = {"contentDirectoryID": allow_library}
|
||||
items = self._plex.fetchItems("/hubs/continueWatching/items",
|
||||
container_start=0,
|
||||
@@ -871,7 +874,10 @@ class Plex:
|
||||
if not self._plex:
|
||||
return None
|
||||
# 请求参数(除黑名单)
|
||||
allow_library = ",".join(map(str, (lib.id for lib in self.get_librarys(hidden=True))))
|
||||
libraries = self.get_librarys(hidden=True)
|
||||
if libraries is None:
|
||||
return None
|
||||
allow_library = ",".join(map(str, (lib.id for lib in libraries)))
|
||||
params = {
|
||||
"contentDirectoryID": allow_library,
|
||||
"count": num,
|
||||
|
||||
@@ -1265,7 +1265,8 @@ class TheMovieDbModule(_ModuleBase):
|
||||
vote_average: float,
|
||||
vote_count: int,
|
||||
release_date: str,
|
||||
page: Optional[int] = 1) -> Optional[List[MediaInfo]]:
|
||||
page: Optional[int] = 1,
|
||||
raise_exception: bool = False) -> Optional[List[MediaInfo]]:
|
||||
"""
|
||||
TMDB发现功能(异步版本)
|
||||
:param mtype: 媒体类型
|
||||
@@ -1291,7 +1292,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
"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,
|
||||
@@ -1303,20 +1304,25 @@ class TheMovieDbModule(_ModuleBase):
|
||||
"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) -> List[MediaInfo]:
|
||||
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)
|
||||
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 []
|
||||
|
||||
@@ -1758,7 +1758,9 @@ class TmdbApi:
|
||||
ret_infos.append(tv)
|
||||
return ret_infos
|
||||
|
||||
async def async_discover_movies(self, params: dict) -> List[dict]:
|
||||
async def async_discover_movies(
|
||||
self, params: dict, raise_exception: bool = False
|
||||
) -> List[dict]:
|
||||
"""
|
||||
发现电影(异步版本)
|
||||
"""
|
||||
@@ -1771,9 +1773,13 @@ class TmdbApi:
|
||||
return items
|
||||
except Exception as e:
|
||||
logger.error(f"获取电影发现失败:{str(e)}")
|
||||
if raise_exception:
|
||||
raise
|
||||
return []
|
||||
|
||||
async def async_discover_tvs(self, params: dict) -> List[dict]:
|
||||
async def async_discover_tvs(
|
||||
self, params: dict, raise_exception: bool = False
|
||||
) -> List[dict]:
|
||||
"""
|
||||
发现电视剧(异步版本)
|
||||
"""
|
||||
@@ -1786,6 +1792,8 @@ class TmdbApi:
|
||||
return items
|
||||
except Exception as e:
|
||||
logger.error(f"获取电视剧发现失败:{str(e)}")
|
||||
if raise_exception:
|
||||
raise
|
||||
return []
|
||||
|
||||
async def async_search_persons(self, name: str) -> List[dict]:
|
||||
@@ -2006,7 +2014,9 @@ class TmdbApi:
|
||||
logger.error(str(e))
|
||||
return {}
|
||||
|
||||
async def async_discover_trending(self, page: Optional[int] = 1) -> List[dict]:
|
||||
async def async_discover_trending(
|
||||
self, page: Optional[int] = 1, raise_exception: bool = False
|
||||
) -> List[dict]:
|
||||
"""
|
||||
流行趋势(异步版本)
|
||||
"""
|
||||
@@ -2018,6 +2028,8 @@ class TmdbApi:
|
||||
return self._normalize_trending_infos(tmdbinfo)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
if raise_exception:
|
||||
raise
|
||||
return []
|
||||
|
||||
async def async_get_movie_images(
|
||||
|
||||
@@ -332,14 +332,14 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
|
||||
|
||||
def mediaserver_playing(
|
||||
self, server: str, count: Optional[int] = 20, **kwargs
|
||||
) -> List[schemas.MediaServerPlayItem]:
|
||||
) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
server_obj: Optional[TrimeMedia] = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return server_obj.get_resume(num=count) or []
|
||||
return None
|
||||
return server_obj.get_resume(num=count)
|
||||
|
||||
def mediaserver_play_url(
|
||||
self, server: str, item_id: Union[str, int]
|
||||
@@ -359,14 +359,14 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
|
||||
server: Optional[str] = None,
|
||||
count: Optional[int] = 20,
|
||||
**kwargs,
|
||||
) -> List[schemas.MediaServerPlayItem]:
|
||||
) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
server_obj: Optional[TrimeMedia] = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return server_obj.get_latest(num=count) or []
|
||||
return None
|
||||
return server_obj.get_latest(num=count)
|
||||
|
||||
def mediaserver_latest_images(
|
||||
self,
|
||||
|
||||
@@ -163,16 +163,18 @@ class TrimeMedia:
|
||||
|
||||
def get_librarys(
|
||||
self, hidden: Optional[bool] = False
|
||||
) -> List[schemas.MediaServerLibrary]:
|
||||
) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not self.is_authenticated():
|
||||
return []
|
||||
return None
|
||||
if self._userinfo.is_admin == 1:
|
||||
mdb_list = self._api.mdb_list() or []
|
||||
mdb_list = self._api.mdb_list()
|
||||
else:
|
||||
mdb_list = self._api.mediadb_list() or []
|
||||
mdb_list = self._api.mediadb_list()
|
||||
if mdb_list is None:
|
||||
return None
|
||||
self._libraries = {lib.guid: lib for lib in mdb_list}
|
||||
libraries = []
|
||||
for library in self._libraries.values():
|
||||
@@ -584,8 +586,11 @@ class TrimeMedia:
|
||||
"""
|
||||
if not self.is_authenticated():
|
||||
return None
|
||||
items = self._api.play_list()
|
||||
if items is None:
|
||||
return None
|
||||
ret_resume = []
|
||||
for item in self._api.play_list() or []:
|
||||
for item in items:
|
||||
if len(ret_resume) == num:
|
||||
break
|
||||
if self.__is_library_blocked(item.ancestor_guid):
|
||||
@@ -599,14 +604,13 @@ class TrimeMedia:
|
||||
"""
|
||||
if not self.is_authenticated():
|
||||
return None
|
||||
items = (
|
||||
self._api.item_list(
|
||||
page=1,
|
||||
page_size=max(100, num * 5),
|
||||
types=[fnapi.Type.MOVIE, fnapi.Type.TV],
|
||||
)
|
||||
or []
|
||||
items = self._api.item_list(
|
||||
page=1,
|
||||
page_size=max(100, num * 5),
|
||||
types=[fnapi.Type.MOVIE, fnapi.Type.TV],
|
||||
)
|
||||
if items is None:
|
||||
return None
|
||||
latest = []
|
||||
for item in items:
|
||||
if len(latest) == num:
|
||||
|
||||
@@ -304,14 +304,14 @@ class UgreenModule(_ModuleBase, _MediaServerBase[Ugreen]):
|
||||
|
||||
def mediaserver_playing(
|
||||
self, server: str, count: Optional[int] = 20, **kwargs
|
||||
) -> List[schemas.MediaServerPlayItem]:
|
||||
) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
server_obj: Optional[Ugreen] = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return server_obj.get_resume(num=count) or []
|
||||
return None
|
||||
return server_obj.get_resume(num=count)
|
||||
|
||||
def mediaserver_play_url(
|
||||
self, server: str, item_id: Union[str, int]
|
||||
@@ -331,14 +331,14 @@ class UgreenModule(_ModuleBase, _MediaServerBase[Ugreen]):
|
||||
server: Optional[str] = None,
|
||||
count: Optional[int] = 20,
|
||||
**kwargs,
|
||||
) -> List[schemas.MediaServerPlayItem]:
|
||||
) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
server_obj: Optional[Ugreen] = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return server_obj.get_latest(num=count) or []
|
||||
return None
|
||||
return server_obj.get_latest(num=count)
|
||||
|
||||
def mediaserver_latest_images(
|
||||
self,
|
||||
|
||||
@@ -489,15 +489,15 @@ class Api:
|
||||
return None
|
||||
return dict(result.data)
|
||||
|
||||
def media_list(self) -> list[dict]:
|
||||
def media_list(self) -> Optional[list[dict]]:
|
||||
"""
|
||||
获取首页媒体库列表(`media_lib_info_list`)。
|
||||
"""
|
||||
result = self.request("v1/video/homepage/media_list")
|
||||
if not result.success or not isinstance(result.data, Mapping):
|
||||
return []
|
||||
return None
|
||||
items = result.data.get("media_lib_info_list")
|
||||
return items if isinstance(items, list) else []
|
||||
return items if isinstance(items, list) else None
|
||||
|
||||
def media_lib_users(self) -> list[dict]:
|
||||
"""
|
||||
|
||||
@@ -520,7 +520,7 @@ class Ugreen:
|
||||
|
||||
return paths
|
||||
|
||||
def get_librarys(self, hidden: Optional[bool] = False) -> List[schemas.MediaServerLibrary]:
|
||||
def get_librarys(self, hidden: Optional[bool] = False) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
获取绿联影视媒体库列表
|
||||
|
||||
@@ -528,9 +528,11 @@ class Ugreen:
|
||||
:return: 媒体库列表
|
||||
"""
|
||||
if not self.is_authenticated() or not self._api:
|
||||
return []
|
||||
return None
|
||||
|
||||
media_libs = self._api.media_list()
|
||||
if media_libs is None:
|
||||
return None
|
||||
self._library_paths = self.__load_library_paths()
|
||||
libraries = []
|
||||
self._libraries = {}
|
||||
@@ -957,8 +959,8 @@ class Ugreen:
|
||||
|
||||
page_size = max(1, num or 12)
|
||||
data = self._api.recently_played(page=1, page_size=page_size)
|
||||
if not data:
|
||||
return []
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
ret_resume = []
|
||||
for item in data.get("video_arr") or []:
|
||||
@@ -982,8 +984,8 @@ class Ugreen:
|
||||
|
||||
page_size = max(1, num)
|
||||
data = self._api.recently_updated(page=1, page_size=page_size)
|
||||
if not data:
|
||||
return []
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
latest = []
|
||||
for item in data.get("video_arr") or []:
|
||||
|
||||
@@ -276,13 +276,13 @@ class ZSpaceModule(_ModuleBase, _MediaServerBase[ZSpace]):
|
||||
) for season, episodes in seasoninfo.items()]
|
||||
|
||||
def mediaserver_playing(self, server: str, count: Optional[int] = 20,
|
||||
username: Optional[str] = None) -> List[schemas.MediaServerPlayItem]:
|
||||
username: Optional[str] = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
server_obj: ZSpace = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return None
|
||||
return server_obj.get_resume(num=count, username=username)
|
||||
|
||||
def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]:
|
||||
@@ -295,13 +295,13 @@ class ZSpaceModule(_ModuleBase, _MediaServerBase[ZSpace]):
|
||||
return server_obj.get_play_url(item_id)
|
||||
|
||||
def mediaserver_latest(self, server: Optional[str] = None, count: Optional[int] = 20,
|
||||
username: Optional[str] = None) -> List[schemas.MediaServerPlayItem]:
|
||||
username: Optional[str] = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
server_obj: ZSpace = self.get_instance(server)
|
||||
if not server_obj:
|
||||
return []
|
||||
return None
|
||||
return server_obj.get_latest(num=count, username=username)
|
||||
|
||||
def mediaserver_latest_images(self,
|
||||
@@ -324,8 +324,7 @@ class ZSpaceModule(_ModuleBase, _MediaServerBase[ZSpace]):
|
||||
return []
|
||||
|
||||
links = []
|
||||
items: List[schemas.MediaServerPlayItem] = self.mediaserver_latest(server=server, count=count,
|
||||
username=username)
|
||||
items = self.mediaserver_latest(server=server, count=count, username=username) or []
|
||||
for item in items:
|
||||
if item.BackdropImageTags:
|
||||
image_url = server_obj.get_backdrop_url(item_id=item.id,
|
||||
|
||||
@@ -198,39 +198,46 @@ class ZSpace:
|
||||
})
|
||||
return libraries
|
||||
|
||||
def __get_library_views(self, username: Optional[str] = None) -> List[dict]:
|
||||
def __get_library_views(self, username: Optional[str] = None) -> Optional[List[dict]]:
|
||||
"""
|
||||
获取极影视媒体库列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
return None
|
||||
if username:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
if not user:
|
||||
return []
|
||||
return None
|
||||
url = f"{self._host}emby/Users/{user}/Views"
|
||||
try:
|
||||
res = self.__request_utils().get_res(url)
|
||||
if res:
|
||||
return res.json().get("Items")
|
||||
items = res.json().get("Items")
|
||||
return items if isinstance(items, list) else None
|
||||
else:
|
||||
logger.error("Users/Views 未获取到返回数据")
|
||||
return []
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Views 出错:{e}")
|
||||
return []
|
||||
return None
|
||||
|
||||
def get_librarys(self, username: Optional[str] = None, hidden: Optional[bool] = False) -> List[
|
||||
schemas.MediaServerLibrary]:
|
||||
def get_librarys(
|
||||
self,
|
||||
username: Optional[str] = None,
|
||||
hidden: Optional[bool] = False,
|
||||
) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
return None
|
||||
source_libraries = self.__get_library_views(username)
|
||||
if source_libraries is None:
|
||||
return None
|
||||
libraries = []
|
||||
for library in self.__get_library_views(username) or []:
|
||||
for library in source_libraries:
|
||||
if hidden and self._sync_libraries and "all" not in self._sync_libraries \
|
||||
and library.get("Id") not in self._sync_libraries:
|
||||
continue
|
||||
@@ -1085,7 +1092,7 @@ class ZSpace:
|
||||
else:
|
||||
user = self.user
|
||||
if not user:
|
||||
return []
|
||||
return None
|
||||
url = f"{self._host}emby/Users/{user}/Items/Resume"
|
||||
params = {
|
||||
"Limit": 100,
|
||||
@@ -1141,7 +1148,7 @@ class ZSpace:
|
||||
logger.error("Users/Items/Resume 未获取到返回数据")
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items/Resume出错:{e}")
|
||||
return []
|
||||
return None
|
||||
|
||||
def get_latest(self, num: Optional[int] = 20, username: Optional[str] = None) -> Optional[
|
||||
List[schemas.MediaServerPlayItem]]:
|
||||
@@ -1160,7 +1167,7 @@ class ZSpace:
|
||||
else:
|
||||
user = self.user
|
||||
if not user:
|
||||
return []
|
||||
return None
|
||||
url = f"{self._host}emby/Users/{user}/Items"
|
||||
params = {
|
||||
"Recursive": "true",
|
||||
@@ -1208,7 +1215,7 @@ class ZSpace:
|
||||
logger.debug("Users/Items?SortBy=DateCreated 未获取到返回数据")
|
||||
except Exception as e:
|
||||
logger.error(f"连接 Users/Items(DateCreated 排序)出错:{e}")
|
||||
return []
|
||||
return None
|
||||
|
||||
def get_user_library_folders(self):
|
||||
"""
|
||||
|
||||
@@ -3,6 +3,8 @@ testpaths =
|
||||
tests
|
||||
timeout = 120
|
||||
timeout_method = thread
|
||||
asyncio_mode = strict
|
||||
asyncio_default_fixture_loop_scope = function
|
||||
# 仅对「无法在本仓修复根因」的已知上游/三方弃用告警做精确忽略,保持测试输出干净、
|
||||
# 让本仓自身的新告警更醒目。本仓代码引发的告警一律不在此忽略,应在源码/用例处修复。
|
||||
filterwarnings =
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Cython~=3.2.5
|
||||
pylint~=4.0.6
|
||||
pytest~=9.0.3
|
||||
pytest-asyncio~=1.4.0
|
||||
pytest-cov~=7.1.0
|
||||
pytest-timeout~=2.4.0
|
||||
uv~=0.11.23
|
||||
|
||||
@@ -131,14 +131,14 @@ class JellyfinUserResolutionTest(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_get_jellyfin_librarys_returns_empty_when_user_missing(self):
|
||||
def test_get_jellyfin_librarys_reports_failure_when_user_missing(self):
|
||||
client = self._build_client()
|
||||
client.user = None
|
||||
|
||||
with patch.object(jellyfin_module, "RequestUtils") as request_utils_cls:
|
||||
libraries = client._Jellyfin__get_jellyfin_librarys()
|
||||
|
||||
self.assertEqual(libraries, [])
|
||||
self.assertIsNone(libraries)
|
||||
request_utils_cls.assert_not_called()
|
||||
|
||||
def test_get_jellyfin_librarys_uses_normalized_views_url(self):
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.api.endpoints.mediaserver import latest, library, playing
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("endpoint", "chain_method", "kwargs"),
|
||||
[
|
||||
(latest, "latest", {"server": "home", "count": 20}),
|
||||
(playing, "playing", {"server": "home", "count": 12}),
|
||||
(library, "librarys", {"server": "home", "hidden": True}),
|
||||
],
|
||||
)
|
||||
def test_dashboard_media_endpoints_preserve_successful_empty_results(
|
||||
endpoint,
|
||||
chain_method,
|
||||
kwargs,
|
||||
):
|
||||
"""媒体服务器成功返回空列表时,Dashboard 接口应保留真实空结果。"""
|
||||
with patch("app.api.endpoints.mediaserver.MediaServerChain") as chain_cls:
|
||||
getattr(chain_cls.return_value, chain_method).return_value = []
|
||||
|
||||
result = endpoint(
|
||||
**kwargs,
|
||||
userinfo=SimpleNamespace(username="alice"),
|
||||
)
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("endpoint", "chain_method", "kwargs"),
|
||||
[
|
||||
(latest, "latest", {"server": "home", "count": 20}),
|
||||
(playing, "playing", {"server": "home", "count": 12}),
|
||||
(library, "librarys", {"server": "home", "hidden": True}),
|
||||
],
|
||||
)
|
||||
def test_dashboard_media_endpoints_report_upstream_failures(
|
||||
endpoint,
|
||||
chain_method,
|
||||
kwargs,
|
||||
):
|
||||
"""媒体服务器请求失败时,Dashboard 接口不得把 None 折叠为空列表。"""
|
||||
with patch("app.api.endpoints.mediaserver.MediaServerChain") as chain_cls:
|
||||
getattr(chain_cls.return_value, chain_method).return_value = None
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
endpoint(
|
||||
**kwargs,
|
||||
userinfo=SimpleNamespace(username="alice"),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 502
|
||||
assert exc_info.value.detail == "媒体服务器请求失败"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method_name", "run_method"),
|
||||
[
|
||||
("latest", "mediaserver_latest"),
|
||||
("playing", "mediaserver_playing"),
|
||||
("librarys", "mediaserver_librarys"),
|
||||
],
|
||||
)
|
||||
def test_media_server_chain_preserves_none_from_provider(method_name, run_method):
|
||||
"""媒体服务器处理链应保留提供方失败状态,交由接口层转换为明确错误。"""
|
||||
chain = MediaServerChain.__new__(MediaServerChain)
|
||||
chain.run_module = lambda method, **kwargs: None
|
||||
|
||||
result = getattr(chain, method_name)(server="home")
|
||||
|
||||
assert result is None
|
||||
@@ -0,0 +1,110 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.api.endpoints.recommend import tmdb_movies, tmdb_trending, tmdb_tvs
|
||||
from app.modules.themoviedb.tmdbapi import TmdbApi
|
||||
from app.modules.themoviedb.tmdbv3api.exceptions import TMDbException
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("endpoint", "chain_method"),
|
||||
[
|
||||
(tmdb_movies, "async_tmdb_movies"),
|
||||
(tmdb_tvs, "async_tmdb_tvs"),
|
||||
(tmdb_trending, "async_tmdb_trending"),
|
||||
],
|
||||
)
|
||||
async def test_dashboard_recommend_endpoints_preserve_successful_empty_results(
|
||||
endpoint,
|
||||
chain_method,
|
||||
):
|
||||
"""TMDB 成功返回空列表时,推荐卡片接口应保留真实空结果。"""
|
||||
with patch("app.api.endpoints.recommend.RecommendChain") as chain_cls:
|
||||
chain_mock = AsyncMock(return_value=[])
|
||||
setattr(chain_cls.return_value, chain_method, chain_mock)
|
||||
|
||||
result = await endpoint(
|
||||
page=1,
|
||||
_=SimpleNamespace(username="alice"),
|
||||
)
|
||||
|
||||
assert result == []
|
||||
assert chain_mock.await_args.kwargs["raise_exception"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("endpoint", "chain_method"),
|
||||
[
|
||||
(tmdb_movies, "async_tmdb_movies"),
|
||||
(tmdb_tvs, "async_tmdb_tvs"),
|
||||
(tmdb_trending, "async_tmdb_trending"),
|
||||
],
|
||||
)
|
||||
async def test_dashboard_recommend_endpoints_report_upstream_failures(
|
||||
endpoint,
|
||||
chain_method,
|
||||
):
|
||||
"""TMDB 请求异常时,推荐卡片接口应返回明确的网关错误。"""
|
||||
with patch("app.api.endpoints.recommend.RecommendChain") as chain_cls:
|
||||
setattr(
|
||||
chain_cls.return_value,
|
||||
chain_method,
|
||||
AsyncMock(side_effect=TMDbException("remote unavailable")),
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await endpoint(
|
||||
page=1,
|
||||
_=SimpleNamespace(username="alice"),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 502
|
||||
assert exc_info.value.detail == "TMDB请求失败"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("method_name", "dependency_name", "dependency_method", "kwargs"),
|
||||
[
|
||||
(
|
||||
"async_discover_movies",
|
||||
"discover",
|
||||
"async_discover_movies",
|
||||
{"params": {"page": 1}},
|
||||
),
|
||||
(
|
||||
"async_discover_tvs",
|
||||
"discover",
|
||||
"async_discover_tv_shows",
|
||||
{"params": {"page": 1}},
|
||||
),
|
||||
(
|
||||
"async_discover_trending",
|
||||
"trending",
|
||||
"async_all_week",
|
||||
{"page": 1},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_tmdb_recommend_queries_only_propagate_failures_in_strict_mode(
|
||||
method_name,
|
||||
dependency_name,
|
||||
dependency_method,
|
||||
kwargs,
|
||||
):
|
||||
"""推荐 endpoint 的严格模式应保留异常,其他调用方继续沿用空列表降级。"""
|
||||
api = TmdbApi.__new__(TmdbApi)
|
||||
dependency = SimpleNamespace(
|
||||
**{dependency_method: AsyncMock(side_effect=TMDbException("remote unavailable"))}
|
||||
)
|
||||
setattr(api, dependency_name, dependency)
|
||||
|
||||
assert await getattr(api, method_name)(**kwargs) == []
|
||||
|
||||
with pytest.raises(TMDbException, match="remote unavailable"):
|
||||
await getattr(api, method_name)(**kwargs, raise_exception=True)
|
||||
Reference in New Issue
Block a user