mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +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 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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
@@ -21,6 +21,18 @@ from app.utils.media import build_media_key, resolve_media_identity
|
|||||||
router = APIRouter()
|
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="在线播放")
|
@router.get("/play/{itemid:path}", summary="在线播放")
|
||||||
def play_item(
|
def play_item(
|
||||||
itemid: str, _: schemas.TokenPayload = Depends(verify_token)
|
itemid: str, _: schemas.TokenPayload = Depends(verify_token)
|
||||||
@@ -153,11 +165,12 @@ def latest(
|
|||||||
"""
|
"""
|
||||||
获取媒体服务器最新入库条目
|
获取媒体服务器最新入库条目
|
||||||
"""
|
"""
|
||||||
return (
|
return _require_mediaserver_result(
|
||||||
MediaServerChain().latest(
|
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(
|
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(
|
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 import schemas
|
||||||
from app.chain.recommend import RecommendChain
|
from app.chain.recommend import RecommendChain
|
||||||
from app.core.event import eventmanager
|
from app.core.event import eventmanager
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
|
from app.modules.themoviedb.tmdbv3api.exceptions import TMDbException
|
||||||
from app.schemas import RecommendSourceEventData
|
from app.schemas import RecommendSourceEventData
|
||||||
from app.schemas.types import ChainEventType
|
from app.schemas.types import ChainEventType
|
||||||
|
|
||||||
router = APIRouter()
|
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(
|
@router.get(
|
||||||
"/source",
|
"/source",
|
||||||
summary="获取推荐数据源",
|
summary="获取推荐数据源",
|
||||||
@@ -204,7 +216,8 @@ async def tmdb_movies(
|
|||||||
"""
|
"""
|
||||||
浏览TMDB电影信息
|
浏览TMDB电影信息
|
||||||
"""
|
"""
|
||||||
return await RecommendChain().async_tmdb_movies(
|
return await _require_tmdb_result(
|
||||||
|
RecommendChain().async_tmdb_movies(
|
||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
with_genres=with_genres,
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
@@ -214,6 +227,8 @@ async def tmdb_movies(
|
|||||||
vote_count=vote_count,
|
vote_count=vote_count,
|
||||||
release_date=release_date,
|
release_date=release_date,
|
||||||
page=page,
|
page=page,
|
||||||
|
raise_exception=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -233,7 +248,8 @@ async def tmdb_tvs(
|
|||||||
"""
|
"""
|
||||||
浏览TMDB剧集信息
|
浏览TMDB剧集信息
|
||||||
"""
|
"""
|
||||||
return await RecommendChain().async_tmdb_tvs(
|
return await _require_tmdb_result(
|
||||||
|
RecommendChain().async_tmdb_tvs(
|
||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
with_genres=with_genres,
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
@@ -243,6 +259,8 @@ async def tmdb_tvs(
|
|||||||
vote_count=vote_count,
|
vote_count=vote_count,
|
||||||
release_date=release_date,
|
release_date=release_date,
|
||||||
page=page,
|
page=page,
|
||||||
|
raise_exception=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -255,4 +273,6 @@ async def tmdb_trending(
|
|||||||
"""
|
"""
|
||||||
TMDB流行趋势
|
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(
|
def _sign_library_images(
|
||||||
self, libraries: Optional[List[MediaServerLibrary]]
|
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:
|
if library.image:
|
||||||
library.image = self._sign_image_url(library.image)
|
library.image = self._sign_image_url(library.image)
|
||||||
if library.image_list:
|
if library.image_list:
|
||||||
@@ -40,21 +42,23 @@ class MediaServerChain(ChainBase):
|
|||||||
for image in library.image_list
|
for image in library.image_list
|
||||||
if image
|
if image
|
||||||
]
|
]
|
||||||
return libraries or []
|
return libraries
|
||||||
|
|
||||||
def _sign_play_item_images(
|
def _sign_play_item_images(
|
||||||
self, items: Optional[List[MediaServerPlayItem]]
|
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:
|
if item.image:
|
||||||
item.image = self._sign_image_url(item.image)
|
item.image = self._sign_image_url(item.image)
|
||||||
return items or []
|
return items
|
||||||
|
|
||||||
def librarys(self, server: str, username: Optional[str] = None,
|
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)
|
return self.run_module("mediaserver_tv_episodes", server=server, item_id=item_id)
|
||||||
|
|
||||||
def playing(self, server: str, count: Optional[int] = 20,
|
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,
|
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_average: Optional[float] = 0.0,
|
||||||
vote_count: Optional[int] = 0,
|
vote_count: Optional[int] = 0,
|
||||||
release_date: Optional[str] = "",
|
release_date: Optional[str] = "",
|
||||||
page: Optional[int] = 1) -> List[dict]:
|
page: Optional[int] = 1,
|
||||||
|
raise_exception: bool = False) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
异步TMDB热门电影
|
异步TMDB热门电影
|
||||||
"""
|
"""
|
||||||
@@ -326,7 +327,8 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
vote_average=vote_average,
|
vote_average=vote_average,
|
||||||
vote_count=vote_count,
|
vote_count=vote_count,
|
||||||
release_date=release_date,
|
release_date=release_date,
|
||||||
page=page)
|
page=page,
|
||||||
|
raise_exception=raise_exception)
|
||||||
return [movie.to_dict() for movie in movies] if movies else []
|
return [movie.to_dict() for movie in movies] if movies else []
|
||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@@ -339,7 +341,8 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
vote_average: Optional[float] = 0.0,
|
vote_average: Optional[float] = 0.0,
|
||||||
vote_count: Optional[int] = 0,
|
vote_count: Optional[int] = 0,
|
||||||
release_date: Optional[str] = "",
|
release_date: Optional[str] = "",
|
||||||
page: Optional[int] = 1) -> List[dict]:
|
page: Optional[int] = 1,
|
||||||
|
raise_exception: bool = False) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
异步TMDB热门电视剧
|
异步TMDB热门电视剧
|
||||||
"""
|
"""
|
||||||
@@ -352,16 +355,23 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
vote_average=vote_average,
|
vote_average=vote_average,
|
||||||
vote_count=vote_count,
|
vote_count=vote_count,
|
||||||
release_date=release_date,
|
release_date=release_date,
|
||||||
page=page)
|
page=page,
|
||||||
|
raise_exception=raise_exception)
|
||||||
return [tv.to_dict() for tv in tvs] if tvs else []
|
return [tv.to_dict() for tv in tvs] if tvs else []
|
||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
@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流行趋势
|
异步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 []
|
return [info.to_dict() for info in infos] if infos else []
|
||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
|
|||||||
@@ -99,6 +99,8 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"模块不支持测试": "Module does not support testing",
|
"模块不支持测试": "Module does not support testing",
|
||||||
"网络请求失败": "Network request failed",
|
"网络请求失败": "Network request failed",
|
||||||
|
"TMDB请求失败": "TMDB request failed",
|
||||||
|
"媒体服务器请求失败": "Media server request failed",
|
||||||
"附件保存失败": "Failed to save attachment",
|
"附件保存失败": "Failed to save attachment",
|
||||||
"该选择已失效,请重新发起选择": "This selection has expired. Please start the selection again",
|
"该选择已失效,请重新发起选择": "This selection has expired. Please start the selection again",
|
||||||
"会话不存在或无权访问": "The conversation does not exist or you do not have access",
|
"会话不存在或无权访问": "The conversation does not exist or you do not have access",
|
||||||
|
|||||||
@@ -99,6 +99,8 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"模块不支持测试": "模块不支持测试",
|
"模块不支持测试": "模块不支持测试",
|
||||||
"网络请求失败": "网络请求失败",
|
"网络请求失败": "网络请求失败",
|
||||||
|
"TMDB请求失败": "TMDB请求失败",
|
||||||
|
"媒体服务器请求失败": "媒体服务器请求失败",
|
||||||
"豆瓣网络连接失败": "豆瓣网络连接失败",
|
"豆瓣网络连接失败": "豆瓣网络连接失败",
|
||||||
"Bangumi网络连接失败": "Bangumi网络连接失败",
|
"Bangumi网络连接失败": "Bangumi网络连接失败",
|
||||||
"fanart网络连接失败": "fanart网络连接失败",
|
"fanart网络连接失败": "fanart网络连接失败",
|
||||||
|
|||||||
@@ -99,6 +99,8 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"模块不支持测试": "模組不支援測試",
|
"模块不支持测试": "模組不支援測試",
|
||||||
"网络请求失败": "網路請求失敗",
|
"网络请求失败": "網路請求失敗",
|
||||||
|
"TMDB请求失败": "TMDB 請求失敗",
|
||||||
|
"媒体服务器请求失败": "媒體伺服器請求失敗",
|
||||||
"附件保存失败": "附件儲存失敗",
|
"附件保存失败": "附件儲存失敗",
|
||||||
"该选择已失效,请重新发起选择": "此選擇已失效,請重新發起選擇",
|
"该选择已失效,请重新发起选择": "此選擇已失效,請重新發起選擇",
|
||||||
"会话不存在或无权访问": "會話不存在或無權存取",
|
"会话不存在或无权访问": "會話不存在或無權存取",
|
||||||
|
|||||||
@@ -282,13 +282,13 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
|||||||
) for season, episodes in seasoninfo.items()]
|
) for season, episodes in seasoninfo.items()]
|
||||||
|
|
||||||
def mediaserver_playing(self, server: str, count: Optional[int] = 20,
|
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)
|
server_obj: Emby = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_resume(num=count, username=username)
|
return server_obj.get_resume(num=count, username=username)
|
||||||
|
|
||||||
def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]:
|
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)
|
return server_obj.get_season_episode_ids(str(item_id), season)
|
||||||
|
|
||||||
def mediaserver_latest(self, server: Optional[str] = None, count: Optional[int] = 20,
|
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)
|
server_obj: Emby = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_latest(num=count, username=username)
|
return server_obj.get_latest(num=count, username=username)
|
||||||
|
|
||||||
def mediaserver_latest_images(self,
|
def mediaserver_latest_images(self,
|
||||||
@@ -345,8 +345,7 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
links = []
|
links = []
|
||||||
items: List[schemas.MediaServerPlayItem] = self.mediaserver_latest(server=server, count=count,
|
items = self.mediaserver_latest(server=server, count=count, username=username) or []
|
||||||
username=username)
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.BackdropImageTags:
|
if item.BackdropImageTags:
|
||||||
image_url = server_obj.get_backdrop_url(item_id=item.id,
|
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))
|
logger.error(f"连接Library/VirtualFolders/Query 出错:" + str(e))
|
||||||
return []
|
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媒体库列表
|
获取Emby媒体库列表
|
||||||
"""
|
"""
|
||||||
if not self._host or not self._apikey:
|
if not self._host or not self._apikey:
|
||||||
return []
|
return None
|
||||||
if username:
|
if username:
|
||||||
user = self.get_user(username)
|
user = self.get_user(username)
|
||||||
else:
|
else:
|
||||||
user = self.user
|
user = self.user
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
url = f"{self._host}emby/Users/{user}/Views"
|
url = f"{self._host}emby/Users/{user}/Views"
|
||||||
params = {"api_key": self._apikey}
|
params = {"api_key": self._apikey}
|
||||||
try:
|
try:
|
||||||
res = RequestUtils().get_res(url, params)
|
res = RequestUtils().get_res(url, params)
|
||||||
if res:
|
if res:
|
||||||
return res.json().get("Items")
|
items = res.json().get("Items")
|
||||||
|
return items if isinstance(items, list) else None
|
||||||
else:
|
else:
|
||||||
logger.error(f"User/Views 未获取到返回数据")
|
logger.error(f"User/Views 未获取到返回数据")
|
||||||
return []
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"连接User/Views 出错:" + str(e))
|
logger.error(f"连接User/Views 出错:" + str(e))
|
||||||
return []
|
return None
|
||||||
|
|
||||||
def get_librarys(self, username: Optional[str] = None, hidden: Optional[bool] = False) -> List[
|
def get_librarys(
|
||||||
schemas.MediaServerLibrary]:
|
self,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
hidden: Optional[bool] = False,
|
||||||
|
) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||||
"""
|
"""
|
||||||
获取媒体服务器所有媒体库列表
|
获取媒体服务器所有媒体库列表
|
||||||
"""
|
"""
|
||||||
if not self._host or not self._apikey:
|
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 = []
|
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 \
|
if hidden and self._sync_libraries and "all" not in self._sync_libraries \
|
||||||
and library.get("Id") not in self._sync_libraries:
|
and library.get("Id") not in self._sync_libraries:
|
||||||
continue
|
continue
|
||||||
@@ -1206,6 +1215,8 @@ class Emby:
|
|||||||
user = self.get_user(username)
|
user = self.get_user(username)
|
||||||
else:
|
else:
|
||||||
user = self.user
|
user = self.user
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
url = f"{self._host}Users/{user}/Items/Resume"
|
url = f"{self._host}Users/{user}/Items/Resume"
|
||||||
params = {
|
params = {
|
||||||
"Limit": 100,
|
"Limit": 100,
|
||||||
@@ -1266,7 +1277,7 @@ class Emby:
|
|||||||
logger.error(f"Users/Items/Resume 未获取到返回数据")
|
logger.error(f"Users/Items/Resume 未获取到返回数据")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"连接Users/Items/Resume出错:" + str(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[
|
def get_latest(self, num: Optional[int] = 20, username: Optional[str] = None) -> Optional[
|
||||||
List[schemas.MediaServerPlayItem]]:
|
List[schemas.MediaServerPlayItem]]:
|
||||||
@@ -1279,6 +1290,8 @@ class Emby:
|
|||||||
user = self.get_user(username)
|
user = self.get_user(username)
|
||||||
else:
|
else:
|
||||||
user = self.user
|
user = self.user
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
url = f"{self._host}Users/{user}/Items/Latest"
|
url = f"{self._host}Users/{user}/Items/Latest"
|
||||||
params = {
|
params = {
|
||||||
"Limit": 100,
|
"Limit": 100,
|
||||||
@@ -1323,7 +1336,7 @@ class Emby:
|
|||||||
logger.error(f"Users/Items/Latest 未获取到返回数据")
|
logger.error(f"Users/Items/Latest 未获取到返回数据")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"连接Users/Items/Latest出错:" + str(e))
|
logger.error(f"连接Users/Items/Latest出错:" + str(e))
|
||||||
return []
|
return None
|
||||||
|
|
||||||
def get_user_library_folders(self):
|
def get_user_library_folders(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -281,13 +281,14 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
|||||||
) for season, episodes in seasoninfo.items()]
|
) for season, episodes in seasoninfo.items()]
|
||||||
|
|
||||||
def mediaserver_playing(self, server: str,
|
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)
|
server_obj: Jellyfin = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_resume(num=count, username=username)
|
return server_obj.get_resume(num=count, username=username)
|
||||||
|
|
||||||
def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]:
|
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)
|
return server_obj.get_season_episode_ids(str(item_id), season)
|
||||||
|
|
||||||
def mediaserver_latest(self, server: Optional[str] = None, count: Optional[int] = 20,
|
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)
|
server_obj: Jellyfin = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_latest(num=count, username=username)
|
return server_obj.get_latest(num=count, username=username)
|
||||||
|
|
||||||
def mediaserver_latest_images(self,
|
def mediaserver_latest_images(self,
|
||||||
@@ -344,8 +345,7 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
links = []
|
links = []
|
||||||
items: List[schemas.MediaServerPlayItem] = self.mediaserver_latest(server=server, count=count,
|
items = self.mediaserver_latest(server=server, count=count, username=username) or []
|
||||||
username=username)
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.BackdropImageTags:
|
if item.BackdropImageTags:
|
||||||
image_url = server_obj.get_backdrop_url(item_id=item.id,
|
image_url = server_obj.get_backdrop_url(item_id=item.id,
|
||||||
|
|||||||
@@ -114,42 +114,50 @@ class Jellyfin:
|
|||||||
logger.error(f"连接Library/VirtualFolders 出错:" + str(e))
|
logger.error(f"连接Library/VirtualFolders 出错:" + str(e))
|
||||||
return []
|
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媒体库的信息
|
获取Jellyfin媒体库的信息
|
||||||
"""
|
"""
|
||||||
if not self._host or not self._apikey:
|
if not self._host or not self._apikey:
|
||||||
return []
|
return None
|
||||||
if username:
|
if username:
|
||||||
user = self.get_user(username)
|
user = self.get_user(username)
|
||||||
else:
|
else:
|
||||||
user = self.user
|
user = self.user
|
||||||
if not user:
|
if not user:
|
||||||
return []
|
return None
|
||||||
# 使用标准库路径拼接结合统一 URL 规整,避免 host 尾部斜杠缺失导致的寻址偏移。
|
# 使用标准库路径拼接结合统一 URL 规整,避免 host 尾部斜杠缺失导致的寻址偏移。
|
||||||
url = UrlUtils.combine_url(self._host, posixpath.join("Users", str(user), "Views"))
|
url = UrlUtils.combine_url(self._host, posixpath.join("Users", str(user), "Views"))
|
||||||
if not url:
|
if not url:
|
||||||
return []
|
return None
|
||||||
params = {"api_key": self._apikey}
|
params = {"api_key": self._apikey}
|
||||||
try:
|
try:
|
||||||
res = RequestUtils().get_res(url, params)
|
res = RequestUtils().get_res(url, params)
|
||||||
if res:
|
if res:
|
||||||
return res.json().get("Items")
|
items = res.json().get("Items")
|
||||||
|
return items if isinstance(items, list) else None
|
||||||
else:
|
else:
|
||||||
logger.error(f"Users/Views 未获取到返回数据")
|
logger.error(f"Users/Views 未获取到返回数据")
|
||||||
return []
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"连接Users/Views 出错:" + str(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:
|
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 = []
|
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 \
|
if hidden and self._sync_libraries and "all" not in self._sync_libraries \
|
||||||
and library.get("Id") not in self._sync_libraries:
|
and library.get("Id") not in self._sync_libraries:
|
||||||
continue
|
continue
|
||||||
@@ -1027,6 +1035,8 @@ class Jellyfin:
|
|||||||
user = self.get_user(username)
|
user = self.get_user(username)
|
||||||
else:
|
else:
|
||||||
user = self.user
|
user = self.user
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
url = f"{self._host}Users/{user}/Items/Resume"
|
url = f"{self._host}Users/{user}/Items/Resume"
|
||||||
params = {
|
params = {
|
||||||
@@ -1083,7 +1093,7 @@ class Jellyfin:
|
|||||||
logger.error(f"Users/Items/Resume 未获取到返回数据")
|
logger.error(f"Users/Items/Resume 未获取到返回数据")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"连接Users/Items/Resume出错:" + str(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]]:
|
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)
|
user = self.get_user(username)
|
||||||
else:
|
else:
|
||||||
user = self.user
|
user = self.user
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
url = f"{self._host}Users/{user}/Items/Latest"
|
url = f"{self._host}Users/{user}/Items/Latest"
|
||||||
params = {
|
params = {
|
||||||
"Limit": 100,
|
"Limit": 100,
|
||||||
@@ -1136,7 +1148,7 @@ class Jellyfin:
|
|||||||
logger.error(f"Users/Items/Latest 未获取到返回数据")
|
logger.error(f"Users/Items/Latest 未获取到返回数据")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"连接Users/Items/Latest出错:" + str(e))
|
logger.error(f"连接Users/Items/Latest出错:" + str(e))
|
||||||
return []
|
return None
|
||||||
|
|
||||||
def get_user_library_folders(self):
|
def get_user_library_folders(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -293,23 +293,23 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
|
|||||||
) for season, episodes in seasoninfo.items()]
|
) for season, episodes in seasoninfo.items()]
|
||||||
|
|
||||||
def mediaserver_playing(self, server: str, count: Optional[int] = 20,
|
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)
|
server_obj: Plex = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_resume(num=count)
|
return server_obj.get_resume(num=count)
|
||||||
|
|
||||||
def mediaserver_latest(self, server: Optional[str] = None, count: Optional[int] = 20,
|
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)
|
server_obj: Plex = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_latest(num=count)
|
return server_obj.get_latest(num=count)
|
||||||
|
|
||||||
def mediaserver_latest_images(self,
|
def mediaserver_latest_images(self,
|
||||||
@@ -331,8 +331,7 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
links = []
|
links = []
|
||||||
items: List[schemas.MediaServerPlayItem] = self.mediaserver_latest(server=server, count=count,
|
items = self.mediaserver_latest(server=server, count=count, username=username) or []
|
||||||
username=username)
|
|
||||||
for item in items:
|
for item in items:
|
||||||
link = server_obj.get_remote_image_by_id(item_id=item.id,
|
link = server_obj.get_remote_image_by_id(item_id=item.id,
|
||||||
image_type="Backdrop",
|
image_type="Backdrop",
|
||||||
|
|||||||
@@ -122,17 +122,17 @@ class Plex:
|
|||||||
return [f"{self._host.rstrip('/') + url}?X-Plex-Token={self._token}" for url in
|
return [f"{self._host.rstrip('/') + url}?X-Plex-Token={self._token}" for url in
|
||||||
list(poster_urls.keys())[:total_size]]
|
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:
|
if not self._plex:
|
||||||
return []
|
return None
|
||||||
try:
|
try:
|
||||||
self._libraries = self._plex.library.sections()
|
self._libraries = self._plex.library.sections()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"获取媒体服务器所有媒体库列表出错:{str(err)}")
|
logger.error(f"获取媒体服务器所有媒体库列表出错:{str(err)}")
|
||||||
return []
|
return None
|
||||||
libraries = []
|
libraries = []
|
||||||
for library in self._libraries:
|
for library in self._libraries:
|
||||||
if hidden and self._sync_libraries and "all" not in self._sync_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()
|
sections = self._plex.library.sections()
|
||||||
movie_count = tv_count = episode_count = 0
|
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:
|
for sec in sections:
|
||||||
if str(sec.key) not in allow_library:
|
if str(sec.key) not in allow_library:
|
||||||
continue
|
continue
|
||||||
@@ -832,9 +832,12 @@ class Plex:
|
|||||||
获取继续观看的媒体
|
获取继续观看的媒体
|
||||||
"""
|
"""
|
||||||
if not self._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}
|
params = {"contentDirectoryID": allow_library}
|
||||||
items = self._plex.fetchItems("/hubs/continueWatching/items",
|
items = self._plex.fetchItems("/hubs/continueWatching/items",
|
||||||
container_start=0,
|
container_start=0,
|
||||||
@@ -871,7 +874,10 @@ class Plex:
|
|||||||
if not self._plex:
|
if not self._plex:
|
||||||
return None
|
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 = {
|
params = {
|
||||||
"contentDirectoryID": allow_library,
|
"contentDirectoryID": allow_library,
|
||||||
"count": num,
|
"count": num,
|
||||||
|
|||||||
@@ -1265,7 +1265,8 @@ class TheMovieDbModule(_ModuleBase):
|
|||||||
vote_average: float,
|
vote_average: float,
|
||||||
vote_count: int,
|
vote_count: int,
|
||||||
release_date: str,
|
release_date: str,
|
||||||
page: Optional[int] = 1) -> Optional[List[MediaInfo]]:
|
page: Optional[int] = 1,
|
||||||
|
raise_exception: bool = False) -> Optional[List[MediaInfo]]:
|
||||||
"""
|
"""
|
||||||
TMDB发现功能(异步版本)
|
TMDB发现功能(异步版本)
|
||||||
:param mtype: 媒体类型
|
:param mtype: 媒体类型
|
||||||
@@ -1291,7 +1292,7 @@ class TheMovieDbModule(_ModuleBase):
|
|||||||
"vote_count.gte": vote_count,
|
"vote_count.gte": vote_count,
|
||||||
"release_date.gte": release_date,
|
"release_date.gte": release_date,
|
||||||
"page": page
|
"page": page
|
||||||
})
|
}, raise_exception=raise_exception)
|
||||||
elif mtype == MediaType.TV:
|
elif mtype == MediaType.TV:
|
||||||
infos = await self.tmdb.async_discover_tvs({
|
infos = await self.tmdb.async_discover_tvs({
|
||||||
"sort_by": sort_by,
|
"sort_by": sort_by,
|
||||||
@@ -1303,20 +1304,25 @@ class TheMovieDbModule(_ModuleBase):
|
|||||||
"vote_count.gte": vote_count,
|
"vote_count.gte": vote_count,
|
||||||
"first_air_date.gte": release_date,
|
"first_air_date.gte": release_date,
|
||||||
"page": page
|
"page": page
|
||||||
})
|
}, raise_exception=raise_exception)
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
if infos:
|
if infos:
|
||||||
return [MediaInfo(tmdb_info=info) for info in infos]
|
return [MediaInfo(tmdb_info=info) for info in infos]
|
||||||
return []
|
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流行趋势(异步版本)
|
TMDB流行趋势(异步版本)
|
||||||
:param page: 第几页
|
:param page: 第几页
|
||||||
:return: TMDB信息列表
|
: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:
|
if trending:
|
||||||
return [MediaInfo(tmdb_info=info) for info in trending]
|
return [MediaInfo(tmdb_info=info) for info in trending]
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -1758,7 +1758,9 @@ class TmdbApi:
|
|||||||
ret_infos.append(tv)
|
ret_infos.append(tv)
|
||||||
return ret_infos
|
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
|
return items
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取电影发现失败:{str(e)}")
|
logger.error(f"获取电影发现失败:{str(e)}")
|
||||||
|
if raise_exception:
|
||||||
|
raise
|
||||||
return []
|
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
|
return items
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取电视剧发现失败:{str(e)}")
|
logger.error(f"获取电视剧发现失败:{str(e)}")
|
||||||
|
if raise_exception:
|
||||||
|
raise
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def async_search_persons(self, name: str) -> List[dict]:
|
async def async_search_persons(self, name: str) -> List[dict]:
|
||||||
@@ -2006,7 +2014,9 @@ class TmdbApi:
|
|||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
return {}
|
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)
|
return self._normalize_trending_infos(tmdbinfo)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
|
if raise_exception:
|
||||||
|
raise
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def async_get_movie_images(
|
async def async_get_movie_images(
|
||||||
|
|||||||
@@ -332,14 +332,14 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
|
|||||||
|
|
||||||
def mediaserver_playing(
|
def mediaserver_playing(
|
||||||
self, server: str, count: Optional[int] = 20, **kwargs
|
self, server: str, count: Optional[int] = 20, **kwargs
|
||||||
) -> List[schemas.MediaServerPlayItem]:
|
) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||||
"""
|
"""
|
||||||
获取媒体服务器正在播放信息
|
获取媒体服务器正在播放信息
|
||||||
"""
|
"""
|
||||||
server_obj: Optional[TrimeMedia] = self.get_instance(server)
|
server_obj: Optional[TrimeMedia] = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_resume(num=count) or []
|
return server_obj.get_resume(num=count)
|
||||||
|
|
||||||
def mediaserver_play_url(
|
def mediaserver_play_url(
|
||||||
self, server: str, item_id: Union[str, int]
|
self, server: str, item_id: Union[str, int]
|
||||||
@@ -359,14 +359,14 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
|
|||||||
server: Optional[str] = None,
|
server: Optional[str] = None,
|
||||||
count: Optional[int] = 20,
|
count: Optional[int] = 20,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> List[schemas.MediaServerPlayItem]:
|
) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||||
"""
|
"""
|
||||||
获取媒体服务器最新入库条目
|
获取媒体服务器最新入库条目
|
||||||
"""
|
"""
|
||||||
server_obj: Optional[TrimeMedia] = self.get_instance(server)
|
server_obj: Optional[TrimeMedia] = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_latest(num=count) or []
|
return server_obj.get_latest(num=count)
|
||||||
|
|
||||||
def mediaserver_latest_images(
|
def mediaserver_latest_images(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -163,16 +163,18 @@ class TrimeMedia:
|
|||||||
|
|
||||||
def get_librarys(
|
def get_librarys(
|
||||||
self, hidden: Optional[bool] = False
|
self, hidden: Optional[bool] = False
|
||||||
) -> List[schemas.MediaServerLibrary]:
|
) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||||
"""
|
"""
|
||||||
获取媒体服务器所有媒体库列表
|
获取媒体服务器所有媒体库列表
|
||||||
"""
|
"""
|
||||||
if not self.is_authenticated():
|
if not self.is_authenticated():
|
||||||
return []
|
return None
|
||||||
if self._userinfo.is_admin == 1:
|
if self._userinfo.is_admin == 1:
|
||||||
mdb_list = self._api.mdb_list() or []
|
mdb_list = self._api.mdb_list()
|
||||||
else:
|
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}
|
self._libraries = {lib.guid: lib for lib in mdb_list}
|
||||||
libraries = []
|
libraries = []
|
||||||
for library in self._libraries.values():
|
for library in self._libraries.values():
|
||||||
@@ -584,8 +586,11 @@ class TrimeMedia:
|
|||||||
"""
|
"""
|
||||||
if not self.is_authenticated():
|
if not self.is_authenticated():
|
||||||
return None
|
return None
|
||||||
|
items = self._api.play_list()
|
||||||
|
if items is None:
|
||||||
|
return None
|
||||||
ret_resume = []
|
ret_resume = []
|
||||||
for item in self._api.play_list() or []:
|
for item in items:
|
||||||
if len(ret_resume) == num:
|
if len(ret_resume) == num:
|
||||||
break
|
break
|
||||||
if self.__is_library_blocked(item.ancestor_guid):
|
if self.__is_library_blocked(item.ancestor_guid):
|
||||||
@@ -599,14 +604,13 @@ class TrimeMedia:
|
|||||||
"""
|
"""
|
||||||
if not self.is_authenticated():
|
if not self.is_authenticated():
|
||||||
return None
|
return None
|
||||||
items = (
|
items = self._api.item_list(
|
||||||
self._api.item_list(
|
|
||||||
page=1,
|
page=1,
|
||||||
page_size=max(100, num * 5),
|
page_size=max(100, num * 5),
|
||||||
types=[fnapi.Type.MOVIE, fnapi.Type.TV],
|
types=[fnapi.Type.MOVIE, fnapi.Type.TV],
|
||||||
)
|
)
|
||||||
or []
|
if items is None:
|
||||||
)
|
return None
|
||||||
latest = []
|
latest = []
|
||||||
for item in items:
|
for item in items:
|
||||||
if len(latest) == num:
|
if len(latest) == num:
|
||||||
|
|||||||
@@ -304,14 +304,14 @@ class UgreenModule(_ModuleBase, _MediaServerBase[Ugreen]):
|
|||||||
|
|
||||||
def mediaserver_playing(
|
def mediaserver_playing(
|
||||||
self, server: str, count: Optional[int] = 20, **kwargs
|
self, server: str, count: Optional[int] = 20, **kwargs
|
||||||
) -> List[schemas.MediaServerPlayItem]:
|
) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||||
"""
|
"""
|
||||||
获取媒体服务器正在播放信息
|
获取媒体服务器正在播放信息
|
||||||
"""
|
"""
|
||||||
server_obj: Optional[Ugreen] = self.get_instance(server)
|
server_obj: Optional[Ugreen] = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_resume(num=count) or []
|
return server_obj.get_resume(num=count)
|
||||||
|
|
||||||
def mediaserver_play_url(
|
def mediaserver_play_url(
|
||||||
self, server: str, item_id: Union[str, int]
|
self, server: str, item_id: Union[str, int]
|
||||||
@@ -331,14 +331,14 @@ class UgreenModule(_ModuleBase, _MediaServerBase[Ugreen]):
|
|||||||
server: Optional[str] = None,
|
server: Optional[str] = None,
|
||||||
count: Optional[int] = 20,
|
count: Optional[int] = 20,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> List[schemas.MediaServerPlayItem]:
|
) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||||
"""
|
"""
|
||||||
获取媒体服务器最新入库条目
|
获取媒体服务器最新入库条目
|
||||||
"""
|
"""
|
||||||
server_obj: Optional[Ugreen] = self.get_instance(server)
|
server_obj: Optional[Ugreen] = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_latest(num=count) or []
|
return server_obj.get_latest(num=count)
|
||||||
|
|
||||||
def mediaserver_latest_images(
|
def mediaserver_latest_images(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -489,15 +489,15 @@ class Api:
|
|||||||
return None
|
return None
|
||||||
return dict(result.data)
|
return dict(result.data)
|
||||||
|
|
||||||
def media_list(self) -> list[dict]:
|
def media_list(self) -> Optional[list[dict]]:
|
||||||
"""
|
"""
|
||||||
获取首页媒体库列表(`media_lib_info_list`)。
|
获取首页媒体库列表(`media_lib_info_list`)。
|
||||||
"""
|
"""
|
||||||
result = self.request("v1/video/homepage/media_list")
|
result = self.request("v1/video/homepage/media_list")
|
||||||
if not result.success or not isinstance(result.data, Mapping):
|
if not result.success or not isinstance(result.data, Mapping):
|
||||||
return []
|
return None
|
||||||
items = result.data.get("media_lib_info_list")
|
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]:
|
def media_lib_users(self) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ class Ugreen:
|
|||||||
|
|
||||||
return paths
|
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: 媒体库列表
|
:return: 媒体库列表
|
||||||
"""
|
"""
|
||||||
if not self.is_authenticated() or not self._api:
|
if not self.is_authenticated() or not self._api:
|
||||||
return []
|
return None
|
||||||
|
|
||||||
media_libs = self._api.media_list()
|
media_libs = self._api.media_list()
|
||||||
|
if media_libs is None:
|
||||||
|
return None
|
||||||
self._library_paths = self.__load_library_paths()
|
self._library_paths = self.__load_library_paths()
|
||||||
libraries = []
|
libraries = []
|
||||||
self._libraries = {}
|
self._libraries = {}
|
||||||
@@ -957,8 +959,8 @@ class Ugreen:
|
|||||||
|
|
||||||
page_size = max(1, num or 12)
|
page_size = max(1, num or 12)
|
||||||
data = self._api.recently_played(page=1, page_size=page_size)
|
data = self._api.recently_played(page=1, page_size=page_size)
|
||||||
if not data:
|
if data is None:
|
||||||
return []
|
return None
|
||||||
|
|
||||||
ret_resume = []
|
ret_resume = []
|
||||||
for item in data.get("video_arr") or []:
|
for item in data.get("video_arr") or []:
|
||||||
@@ -982,8 +984,8 @@ class Ugreen:
|
|||||||
|
|
||||||
page_size = max(1, num)
|
page_size = max(1, num)
|
||||||
data = self._api.recently_updated(page=1, page_size=page_size)
|
data = self._api.recently_updated(page=1, page_size=page_size)
|
||||||
if not data:
|
if data is None:
|
||||||
return []
|
return None
|
||||||
|
|
||||||
latest = []
|
latest = []
|
||||||
for item in data.get("video_arr") or []:
|
for item in data.get("video_arr") or []:
|
||||||
|
|||||||
@@ -276,13 +276,13 @@ class ZSpaceModule(_ModuleBase, _MediaServerBase[ZSpace]):
|
|||||||
) for season, episodes in seasoninfo.items()]
|
) for season, episodes in seasoninfo.items()]
|
||||||
|
|
||||||
def mediaserver_playing(self, server: str, count: Optional[int] = 20,
|
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)
|
server_obj: ZSpace = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_resume(num=count, username=username)
|
return server_obj.get_resume(num=count, username=username)
|
||||||
|
|
||||||
def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]:
|
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)
|
return server_obj.get_play_url(item_id)
|
||||||
|
|
||||||
def mediaserver_latest(self, server: Optional[str] = None, count: Optional[int] = 20,
|
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)
|
server_obj: ZSpace = self.get_instance(server)
|
||||||
if not server_obj:
|
if not server_obj:
|
||||||
return []
|
return None
|
||||||
return server_obj.get_latest(num=count, username=username)
|
return server_obj.get_latest(num=count, username=username)
|
||||||
|
|
||||||
def mediaserver_latest_images(self,
|
def mediaserver_latest_images(self,
|
||||||
@@ -324,8 +324,7 @@ class ZSpaceModule(_ModuleBase, _MediaServerBase[ZSpace]):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
links = []
|
links = []
|
||||||
items: List[schemas.MediaServerPlayItem] = self.mediaserver_latest(server=server, count=count,
|
items = self.mediaserver_latest(server=server, count=count, username=username) or []
|
||||||
username=username)
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.BackdropImageTags:
|
if item.BackdropImageTags:
|
||||||
image_url = server_obj.get_backdrop_url(item_id=item.id,
|
image_url = server_obj.get_backdrop_url(item_id=item.id,
|
||||||
|
|||||||
@@ -198,39 +198,46 @@ class ZSpace:
|
|||||||
})
|
})
|
||||||
return libraries
|
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:
|
if not self._host or not self._apikey:
|
||||||
return []
|
return None
|
||||||
if username:
|
if username:
|
||||||
user = self.get_user(username)
|
user = self.get_user(username)
|
||||||
else:
|
else:
|
||||||
user = self.user
|
user = self.user
|
||||||
if not user:
|
if not user:
|
||||||
return []
|
return None
|
||||||
url = f"{self._host}emby/Users/{user}/Views"
|
url = f"{self._host}emby/Users/{user}/Views"
|
||||||
try:
|
try:
|
||||||
res = self.__request_utils().get_res(url)
|
res = self.__request_utils().get_res(url)
|
||||||
if res:
|
if res:
|
||||||
return res.json().get("Items")
|
items = res.json().get("Items")
|
||||||
|
return items if isinstance(items, list) else None
|
||||||
else:
|
else:
|
||||||
logger.error("Users/Views 未获取到返回数据")
|
logger.error("Users/Views 未获取到返回数据")
|
||||||
return []
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"连接Users/Views 出错:{e}")
|
logger.error(f"连接Users/Views 出错:{e}")
|
||||||
return []
|
return None
|
||||||
|
|
||||||
def get_librarys(self, username: Optional[str] = None, hidden: Optional[bool] = False) -> List[
|
def get_librarys(
|
||||||
schemas.MediaServerLibrary]:
|
self,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
hidden: Optional[bool] = False,
|
||||||
|
) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||||
"""
|
"""
|
||||||
获取媒体服务器所有媒体库列表
|
获取媒体服务器所有媒体库列表
|
||||||
"""
|
"""
|
||||||
if not self._host or not self._apikey:
|
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 = []
|
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 \
|
if hidden and self._sync_libraries and "all" not in self._sync_libraries \
|
||||||
and library.get("Id") not in self._sync_libraries:
|
and library.get("Id") not in self._sync_libraries:
|
||||||
continue
|
continue
|
||||||
@@ -1085,7 +1092,7 @@ class ZSpace:
|
|||||||
else:
|
else:
|
||||||
user = self.user
|
user = self.user
|
||||||
if not user:
|
if not user:
|
||||||
return []
|
return None
|
||||||
url = f"{self._host}emby/Users/{user}/Items/Resume"
|
url = f"{self._host}emby/Users/{user}/Items/Resume"
|
||||||
params = {
|
params = {
|
||||||
"Limit": 100,
|
"Limit": 100,
|
||||||
@@ -1141,7 +1148,7 @@ class ZSpace:
|
|||||||
logger.error("Users/Items/Resume 未获取到返回数据")
|
logger.error("Users/Items/Resume 未获取到返回数据")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"连接Users/Items/Resume出错:{e}")
|
logger.error(f"连接Users/Items/Resume出错:{e}")
|
||||||
return []
|
return None
|
||||||
|
|
||||||
def get_latest(self, num: Optional[int] = 20, username: Optional[str] = None) -> Optional[
|
def get_latest(self, num: Optional[int] = 20, username: Optional[str] = None) -> Optional[
|
||||||
List[schemas.MediaServerPlayItem]]:
|
List[schemas.MediaServerPlayItem]]:
|
||||||
@@ -1160,7 +1167,7 @@ class ZSpace:
|
|||||||
else:
|
else:
|
||||||
user = self.user
|
user = self.user
|
||||||
if not user:
|
if not user:
|
||||||
return []
|
return None
|
||||||
url = f"{self._host}emby/Users/{user}/Items"
|
url = f"{self._host}emby/Users/{user}/Items"
|
||||||
params = {
|
params = {
|
||||||
"Recursive": "true",
|
"Recursive": "true",
|
||||||
@@ -1208,7 +1215,7 @@ class ZSpace:
|
|||||||
logger.debug("Users/Items?SortBy=DateCreated 未获取到返回数据")
|
logger.debug("Users/Items?SortBy=DateCreated 未获取到返回数据")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"连接 Users/Items(DateCreated 排序)出错:{e}")
|
logger.error(f"连接 Users/Items(DateCreated 排序)出错:{e}")
|
||||||
return []
|
return None
|
||||||
|
|
||||||
def get_user_library_folders(self):
|
def get_user_library_folders(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ testpaths =
|
|||||||
tests
|
tests
|
||||||
timeout = 120
|
timeout = 120
|
||||||
timeout_method = thread
|
timeout_method = thread
|
||||||
|
asyncio_mode = strict
|
||||||
|
asyncio_default_fixture_loop_scope = function
|
||||||
# 仅对「无法在本仓修复根因」的已知上游/三方弃用告警做精确忽略,保持测试输出干净、
|
# 仅对「无法在本仓修复根因」的已知上游/三方弃用告警做精确忽略,保持测试输出干净、
|
||||||
# 让本仓自身的新告警更醒目。本仓代码引发的告警一律不在此忽略,应在源码/用例处修复。
|
# 让本仓自身的新告警更醒目。本仓代码引发的告警一律不在此忽略,应在源码/用例处修复。
|
||||||
filterwarnings =
|
filterwarnings =
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Cython~=3.2.5
|
Cython~=3.2.5
|
||||||
pylint~=4.0.6
|
pylint~=4.0.6
|
||||||
pytest~=9.0.3
|
pytest~=9.0.3
|
||||||
|
pytest-asyncio~=1.4.0
|
||||||
pytest-cov~=7.1.0
|
pytest-cov~=7.1.0
|
||||||
pytest-timeout~=2.4.0
|
pytest-timeout~=2.4.0
|
||||||
uv~=0.11.23
|
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 = self._build_client()
|
||||||
client.user = None
|
client.user = None
|
||||||
|
|
||||||
with patch.object(jellyfin_module, "RequestUtils") as request_utils_cls:
|
with patch.object(jellyfin_module, "RequestUtils") as request_utils_cls:
|
||||||
libraries = client._Jellyfin__get_jellyfin_librarys()
|
libraries = client._Jellyfin__get_jellyfin_librarys()
|
||||||
|
|
||||||
self.assertEqual(libraries, [])
|
self.assertIsNone(libraries)
|
||||||
request_utils_cls.assert_not_called()
|
request_utils_cls.assert_not_called()
|
||||||
|
|
||||||
def test_get_jellyfin_librarys_uses_normalized_views_url(self):
|
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