Merge pull request #4970 from cddjr/fix_trimemedia

This commit is contained in:
jxxghp
2025-09-18 15:55:46 +08:00
committed by GitHub
3 changed files with 106 additions and 63 deletions
+17 -13
View File
@@ -154,7 +154,7 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
""" """
source = args.get("source") source = args.get("source")
if source: if source:
server: TrimeMedia = self.get_instance(source) server: Optional[TrimeMedia] = self.get_instance(source)
if not server: if not server:
return None return None
result = server.get_webhook_message(body) result = server.get_webhook_message(body)
@@ -247,7 +247,7 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
媒体数量统计 媒体数量统计
""" """
if server: if server:
server_obj: TrimeMedia = self.get_instance(server) server_obj: Optional[TrimeMedia] = self.get_instance(server)
if not server_obj: if not server_obj:
return None return None
servers = [server_obj] servers = [server_obj]
@@ -268,7 +268,7 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
""" """
媒体库列表 媒体库列表
""" """
server_obj: TrimeMedia = self.get_instance(server) server_obj: Optional[TrimeMedia] = self.get_instance(server)
if server_obj: if server_obj:
return server_obj.get_librarys(hidden=hidden) return server_obj.get_librarys(hidden=hidden)
return None return None
@@ -290,7 +290,7 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
:return: 返回一个生成器对象,用于逐步获取媒体服务器中的项目 :return: 返回一个生成器对象,用于逐步获取媒体服务器中的项目
""" """
server_obj: TrimeMedia = self.get_instance(server) server_obj: Optional[TrimeMedia] = self.get_instance(server)
if server_obj: if server_obj:
return server_obj.get_items(library_id, start_index, limit) return server_obj.get_items(library_id, start_index, limit)
return None return None
@@ -301,7 +301,7 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
""" """
媒体库项目详情 媒体库项目详情
""" """
server_obj: TrimeMedia = self.get_instance(server) server_obj: Optional[TrimeMedia] = self.get_instance(server)
if server_obj: if server_obj:
return server_obj.get_iteminfo(item_id) return server_obj.get_iteminfo(item_id)
return None return None
@@ -312,7 +312,9 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
""" """
获取剧集信息 获取剧集信息
""" """
server_obj: TrimeMedia = self.get_instance(server) if not isinstance(item_id, str):
return None
server_obj: Optional[TrimeMedia] = self.get_instance(server)
if not server_obj: if not server_obj:
return None return None
_, seasoninfo = server_obj.get_tv_episodes(item_id=item_id) _, seasoninfo = server_obj.get_tv_episodes(item_id=item_id)
@@ -329,10 +331,10 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
""" """
获取媒体服务器正在播放信息 获取媒体服务器正在播放信息
""" """
server_obj: TrimeMedia = self.get_instance(server) server_obj: Optional[TrimeMedia] = self.get_instance(server)
if not server_obj: if not server_obj:
return [] return []
return server_obj.get_resume(num=count) return server_obj.get_resume(num=count) or []
def mediaserver_play_url( def mediaserver_play_url(
self, server: str, item_id: Union[str, int] self, server: str, item_id: Union[str, int]
@@ -340,7 +342,9 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
""" """
获取媒体库播放地址 获取媒体库播放地址
""" """
server_obj: TrimeMedia = self.get_instance(server) if not isinstance(item_id, str):
return None
server_obj: Optional[TrimeMedia] = self.get_instance(server)
if not server_obj: if not server_obj:
return None return None
return server_obj.get_play_url(item_id) return server_obj.get_play_url(item_id)
@@ -354,10 +358,10 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
""" """
获取媒体服务器最新入库条目 获取媒体服务器最新入库条目
""" """
server_obj: TrimeMedia = self.get_instance(server) server_obj: Optional[TrimeMedia] = self.get_instance(server)
if not server_obj: if not server_obj:
return [] return []
return server_obj.get_latest(num=count) return server_obj.get_latest(num=count) or []
def mediaserver_latest_images( def mediaserver_latest_images(
self, self,
@@ -374,7 +378,7 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
:param remote: True为外网链接, False为内网链接 :param remote: True为外网链接, False为内网链接
:return: 图片链接列表 :return: 图片链接列表
""" """
server_obj: TrimeMedia = self.get_instance(server) server_obj: Optional[TrimeMedia] = self.get_instance(server)
if not server_obj: if not server_obj:
return [] return []
return server_obj.get_latest_backdrops(num=count, remote=remote) return server_obj.get_latest_backdrops(num=count, remote=remote) or []
+38 -26
View File
@@ -140,13 +140,13 @@ class Api:
self._token: Optional[str] = None self._token: Optional[str] = None
self._version: Optional[Version] = None self._version: Optional[Version] = None
self._session = requests.Session() self._session = requests.Session()
self._request_utils = RequestUtils(session=self._session) self._request_utils = RequestUtils(session=self._session, timeout=10)
def sys_version(self) -> Optional[Version]: def sys_version(self) -> Optional[Version]:
""" """
飞牛影视版本号 飞牛影视版本号
""" """
if (res := self.__request_api("/sys/version")) and res.success: if (res := self.request("/sys/version")) and res.success:
if res.data: if res.data:
self._version = Version( self._version = Version(
frontend=res.data.get("version"), frontend=res.data.get("version"),
@@ -162,7 +162,7 @@ class Api:
:return: 成功返回token 否则返回None :return: 成功返回token 否则返回None
""" """
if ( if (
res := self.__request_api( res := self.request(
"/login", "/login",
data={ data={
"username": username, "username": username,
@@ -178,7 +178,9 @@ class Api:
""" """
退出账号 退出账号
""" """
if (res := self.__request_api("/user/logout", method="post")) and res.success: if not self._token:
return True
if (res := self.request("/user/logout", method="post")) and res.success:
if res.data: if res.data:
self._token = None self._token = None
return True return True
@@ -188,7 +190,9 @@ class Api:
""" """
用户列表(仅管理员有权访问) 用户列表(仅管理员有权访问)
""" """
if (res := self.__request_api("/manager/user/list")) and res.success: if (res := self.request("/manager/user/list")) and res.success:
if not res.data:
return []
return [ return [
User( User(
guid=info.get("guid"), guid=info.get("guid"),
@@ -203,7 +207,7 @@ class Api:
""" """
当前用户信息 当前用户信息
""" """
if (res := self.__request_api("/user/info")) and res.success: if (res := self.request("/user/info")) and res.success:
_user = User("", "") _user = User("", "")
_user.__dict__.update(res.data) _user.__dict__.update(res.data)
return _user return _user
@@ -213,7 +217,7 @@ class Api:
""" """
媒体数量统计 媒体数量统计
""" """
if (res := self.__request_api("/mediadb/sum")) and res.success: if (res := self.request("/mediadb/sum")) and res.success:
sums = MediaDbSummary() sums = MediaDbSummary()
sums.__dict__.update(res.data) sums.__dict__.update(res.data)
return sums return sums
@@ -223,9 +227,9 @@ class Api:
""" """
媒体库列表(普通用户) 媒体库列表(普通用户)
""" """
if (res := self.__request_api("/mediadb/list")) and res.success: if (res := self.request("/mediadb/list")) and res.success:
_items = [] _items = []
for info in res.data: for info in res.data or []:
mdb = MediaDb( mdb = MediaDb(
guid=info.get("guid"), guid=info.get("guid"),
category=Category(info.get("category")), category=Category(info.get("category")),
@@ -250,9 +254,9 @@ class Api:
""" """
媒体库列表(管理员) 媒体库列表(管理员)
""" """
if (res := self.__request_api("/mdb/list")) and res.success: if (res := self.request("/mdb/list")) and res.success:
_items = [] _items = []
for info in res.data: for info in res.data or []:
mdb = MediaDb( mdb = MediaDb(
guid=info.get("guid"), guid=info.get("guid"),
category=Category(info.get("category")), category=Category(info.get("category")),
@@ -271,7 +275,7 @@ class Api:
""" """
扫描所有媒体库 扫描所有媒体库
""" """
if (res := self.__request_api("/mdb/scanall", method="post")) and res.success: if (res := self.request("/mdb/scanall", method="post")) and res.success:
if res.data: if res.data:
return True return True
return False return False
@@ -280,9 +284,7 @@ class Api:
""" """
扫描指定媒体库 扫描指定媒体库
""" """
if ( if (res := self.request(f"/mdb/scan/{mdb.guid}", data={})) and res.success:
res := self.__request_api(f"/mdb/scan/{mdb.guid}", data={})
) and res.success:
if res.data: if res.data:
return True return True
return False return False
@@ -291,9 +293,7 @@ class Api:
""" """
当前正在运行的任务 当前正在运行的任务
""" """
if ( if (res := self.request("/task/running")) and res.success:
res := self.__request_api("/task/running")
) and res.success:
if res.data: if res.data:
# TODO 具体正在运行的任务 # TODO 具体正在运行的任务
return True return True
@@ -341,7 +341,9 @@ class Api:
if exclude_grouped_video: if exclude_grouped_video:
post["exclude_grouped_video"] = 1 post["exclude_grouped_video"] = 1
if (res := self.__request_api("/item/list", data=post)) and res.success: if (res := self.request("/item/list", data=post)) and res.success:
if not res.data:
return []
return [self.__build_item(info) for info in res.data.get("list", [])] return [self.__build_item(info) for info in res.data.get("list", [])]
return None return None
@@ -350,8 +352,10 @@ class Api:
搜索影片、演员 搜索影片、演员
""" """
if ( if (
res := self.__request_api("/search/list", params={"q": keywords}) res := self.request("/search/list", params={"q": keywords})
) and res.success: ) and res.success:
if not res.data:
return []
return [self.__build_item(info) for info in res.data] return [self.__build_item(info) for info in res.data]
return None return None
@@ -359,7 +363,7 @@ class Api:
""" """
查询媒体详情 查询媒体详情
""" """
if (res := self.__request_api(f"/item/{guid}")) and res.success: if (res := self.request(f"/item/{guid}")) and res.success:
return self.__build_item(res.data) return self.__build_item(res.data)
return None return None
@@ -370,7 +374,7 @@ class Api:
:param delete_file: True删除媒体文件,False仅从媒体库移除 :param delete_file: True删除媒体文件,False仅从媒体库移除
""" """
if ( if (
res := self.__request_api( res := self.request(
f"/item/{guid}", f"/item/{guid}",
method="delete", method="delete",
data={"delete_file": 1 if delete_file else 0, "media_guids": []}, data={"delete_file": 1 if delete_file else 0, "media_guids": []},
@@ -384,7 +388,9 @@ class Api:
""" """
查询季列表 查询季列表
""" """
if (res := self.__request_api(f"/season/list/{tv_guid}")) and res.success: if (res := self.request(f"/season/list/{tv_guid}")) and res.success:
if not res.data:
return []
return [self.__build_item(info) for info in res.data] return [self.__build_item(info) for info in res.data]
return None return None
@@ -392,7 +398,9 @@ class Api:
""" """
查询剧集列表 查询剧集列表
""" """
if (res := self.__request_api(f"/episode/list/{season_guid}")) and res.success: if (res := self.request(f"/episode/list/{season_guid}")) and res.success:
if not res.data:
return []
return [self.__build_item(info) for info in res.data] return [self.__build_item(info) for info in res.data]
return None return None
@@ -400,7 +408,9 @@ class Api:
""" """
继续观看列表 继续观看列表
""" """
if (res := self.__request_api("/play/list")) and res.success: if (res := self.request("/play/list")) and res.success:
if not res.data:
return []
return [self.__build_item(info) for info in res.data] return [self.__build_item(info) for info in res.data]
return None return None
@@ -431,7 +441,7 @@ class Api:
sign = md5.hexdigest() sign = md5.hexdigest()
return f"nonce={nonce}&timestamp={ts}&sign={sign}" return f"nonce={nonce}&timestamp={ts}&sign={sign}"
def __request_api( def request(
self, self,
api: str, api: str,
method: Optional[str] = None, method: Optional[str] = None,
@@ -482,6 +492,8 @@ class Api:
queries_unquoted = None queries_unquoted = None
headers = { headers = {
"User-Agent": settings.USER_AGENT, "User-Agent": settings.USER_AGENT,
"Accept": "application/json",
"Referer": self._host,
"Authorization": self._token, "Authorization": self._token,
"authx": self.__get_authx(api_path, json_body or queries_unquoted), "authx": self.__get_authx(api_path, json_body or queries_unquoted),
} }
+51 -24
View File
@@ -13,6 +13,7 @@ class TrimeMedia:
_password: Optional[str] = None _password: Optional[str] = None
_userinfo: Optional[fnapi.User] = None _userinfo: Optional[fnapi.User] = None
_host: Optional[str] = None
_playhost: Optional[str] = None _playhost: Optional[str] = None
_libraries: dict[str, fnapi.MediaDb] = {} _libraries: dict[str, fnapi.MediaDb] = {}
@@ -34,20 +35,19 @@ class TrimeMedia:
return return
self._username = username self._username = username
self._password = password self._password = password
self._host = host
self._sync_libraries = sync_libraries or [] self._sync_libraries = sync_libraries or []
if (api := self.__create_api(host)) is None: if not self.reconnect():
logger.error(f"请检查服务端地址 {host}") logger.error(f"请检查服务端地址 {host}")
return return
self._api = api if result := self.__create_api(play_host):
if play_api := self.__create_api(play_host): self._playhost = result.api.host
self._playhost = play_api.host result.api.close()
elif play_host: elif play_host:
logger.warning(f"请检查外网播放地址 {play_host}") logger.warning(f"请检查外网播放地址 {play_host}")
self._playhost = UrlUtils.standardize_base_url(play_host).rstrip("/") self._playhost = UrlUtils.standardize_base_url(play_host).rstrip("/")
self.reconnect()
@property @property
def api(self) -> Optional[fnapi.Api]: def api(self) -> Optional[fnapi.Api]:
""" """
@@ -55,14 +55,19 @@ class TrimeMedia:
""" """
return self._api return self._api
class _ApiCreateResult:
api: fnapi.Api
version: fnapi.Version
@staticmethod @staticmethod
def __create_api(host: Optional[str]) -> Optional[fnapi.Api]: def __create_api(host: Optional[str]) -> Optional["TrimeMedia._ApiCreateResult"]:
""" """
创建一个飞牛API 创建一个飞牛API
:param host: 服务端地址 :param host: 服务端地址
:return: 如果地址无效、不可访问则返回None :return: 如果地址无效、不可访问则返回None
""" """
if not host: if not host:
return None return None
api_key = "16CCEB3D-AB42-077D-36A1-F355324E4237" api_key = "16CCEB3D-AB42-077D-36A1-F355324E4237"
@@ -70,21 +75,35 @@ class TrimeMedia:
if not host.endswith("/v"): if not host.endswith("/v"):
# 尝试补上结尾的/v 测试能否正常访问 # 尝试补上结尾的/v 测试能否正常访问
api = fnapi.Api(host + "/v", api_key) res = TrimeMedia._ApiCreateResult()
if api.sys_version(): res.api = fnapi.Api(host + "/v", api_key)
return api if fnver := res.api.sys_version():
res.version = fnver
return res
# 测试用户配置的地址 # 测试用户配置的地址
api = fnapi.Api(host, api_key) res = TrimeMedia._ApiCreateResult()
return api if api.sys_version() else None res.api = fnapi.Api(host, api_key)
if fnver := res.api.sys_version():
res.version = fnver
return res
return None
def close(self): def close(self):
self.disconnect() self.disconnect()
def is_configured(self) -> bool: def is_configured(self) -> bool:
return self._api is not None return bool(self._host and self._username and self._password)
def is_authenticated(self) -> bool: def is_authenticated(self) -> bool:
return self.is_configured() and self._api.token is not None """
是否已登录
"""
return (
self.is_configured()
and self._api is not None
and self._api.token is not None
and self._userinfo is not None
)
def is_inactive(self) -> bool: def is_inactive(self) -> bool:
""" """
@@ -101,10 +120,15 @@ class TrimeMedia:
""" """
if not self.is_configured(): if not self.is_configured():
return False return False
if (fnver := self._api.sys_version()) is None: self.disconnect()
if result := self.__create_api(self._host):
self._api = result.api
# 版本号:0.8.53, 服务版本:0.8.23
logger.debug(
f"版本号:{result.version.frontend}, 服务版本:{result.version.backend}"
)
else:
return False return False
# 版本号:0.8.36, 服务版本:0.8.19
logger.debug(f"版本号:{fnver.frontend}, 服务版本:{fnver.backend}")
if self._api.login(self._username, self._password) is None: if self._api.login(self._username, self._password) is None:
return False return False
self._userinfo = self._api.user_info() self._userinfo = self._api.user_info()
@@ -119,9 +143,10 @@ class TrimeMedia:
""" """
断开与飞牛的连接 断开与飞牛的连接
""" """
if self.is_authenticated(): if self._api:
self._api.logout() self._api.logout()
self._api.close() self._api.close()
self._api = None
self._userinfo = None self._userinfo = None
logger.debug(f"{self._username} 已断开飞牛影视") logger.debug(f"{self._username} 已断开飞牛影视")
@@ -163,7 +188,7 @@ class TrimeMedia:
for img_path in library.posters or [] for img_path in library.posters or []
], ],
link=f"{self._playhost or self._api.host}/library/{library.guid}", link=f"{self._playhost or self._api.host}/library/{library.guid}",
server_type='trimemedia' server_type="trimemedia",
) )
) )
return libraries return libraries
@@ -205,10 +230,12 @@ class TrimeMedia:
return None return None
if not self.is_configured(): if not self.is_configured():
return None return None
feiniu = fnapi.Api(self._api.host, self._api.apikey) if result := self.__create_api(self._host):
if token := feiniu.login(username, password): try:
feiniu.logout() return result.api.login(username, password)
return token finally:
result.api.logout()
result.api.close()
def get_movies( def get_movies(
self, title: str, year: Optional[str] = None, tmdb_id: Optional[int] = None self, title: str, year: Optional[str] = None, tmdb_id: Optional[int] = None
@@ -459,7 +486,7 @@ class TrimeMedia:
if item.duration and item.ts is not None if item.duration and item.ts is not None
else 0 else 0
), ),
server_type='trimemedia', server_type="trimemedia",
) )
def get_items( def get_items(