提升服务端地址的兼容性

This commit is contained in:
景大侠
2025-04-07 16:37:41 +08:00
parent d0b1348c96
commit 8d39cc87f7
3 changed files with 110 additions and 27 deletions
+3 -1
View File
@@ -62,7 +62,9 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
server.reconnect() server.reconnect()
def stop(self): def stop(self):
pass for server in self.get_instances().values():
if server.is_authenticated():
server.disconnect()
def test(self) -> Optional[Tuple[bool, str]]: def test(self) -> Optional[Tuple[bool, str]]:
""" """
+43 -9
View File
@@ -60,6 +60,13 @@ class MediaDbSummary:
total: int = 0 total: int = 0
@dataclass
class Version:
# 飞牛影视版本
frontend: Optional[str] = None
backend: Optional[str] = None
@dataclass @dataclass
class Item: class Item:
guid: str guid: str
@@ -103,6 +110,7 @@ class Api:
"_apikey", "_apikey",
"_api_path", "_api_path",
"_request_utils", "_request_utils",
"_version",
) )
@property @property
@@ -117,13 +125,34 @@ class Api:
def apikey(self) -> str: def apikey(self) -> str:
return self._apikey return self._apikey
@property
def version(self) -> Optional[Version]:
return self._version
def __init__(self, host: str, apikey: str): def __init__(self, host: str, apikey: str):
self._api_path = "/v/api/v1" """
:param host: 飞牛服务端地址,如http://127.0.0.1:5666/v
"""
self._api_path = "/api/v1"
self._host = host.rstrip("/") self._host = host.rstrip("/")
self._apikey = apikey self._apikey = apikey
self._token = None self._token: Optional[str] = None
self._version: Optional[Version] = None
self._request_utils = RequestUtils(session=requests.Session()) self._request_utils = RequestUtils(session=requests.Session())
def sys_version(self) -> Optional[Version]:
"""
飞牛影视版本号
"""
if (res := self.__request_api("/sys/version")) and res.success:
if res.data:
self._version = Version(
frontend=res.data.get("version"),
backend=res.data.get("mediasrvVersion"),
)
return self._version
return None
def login(self, username, password) -> Optional[str]: def login(self, username, password) -> Optional[str]:
""" """
登录飞牛影视 登录飞牛影视
@@ -344,12 +373,12 @@ class Api:
return [self.__build_item(info) for info in res.data] return [self.__build_item(info) for info in res.data]
return None return None
def __get_authx(self, api_path, body: Optional[str]): def __get_authx(self, api_path: str, body: Optional[str]):
""" """
计算消息签名 计算消息签名
""" """
if api_path[0] != "/": if not api_path.startswith("/v"):
api_path = "/" + api_path api_path = "/v" + api_path
nonce = str(random.randint(100000, 999999)) nonce = str(random.randint(100000, 999999))
ts = str(int(time.time() * 1000)) ts = str(int(time.time() * 1000))
md5 = hashlib.md5() md5 = hashlib.md5()
@@ -377,9 +406,12 @@ class Api:
method: Optional[str] = None, method: Optional[str] = None,
params: Optional[dict] = None, params: Optional[dict] = None,
data: Optional[dict] = None, data: Optional[dict] = None,
suppress_log=False,
): ):
""" """
请求飞牛影视API 请求飞牛影视API
:param suppress_log: 是否禁止日志
""" """
@dataclass @dataclass
@@ -432,11 +464,13 @@ class Api:
resp = res.json() resp = res.json()
msg = resp.get("msg") msg = resp.get("msg")
if code := int(resp.get("code", -1)): if code := int(resp.get("code", -1)):
logger.error(f"请求接口 {api_path} 失败,错误码:{code} {msg}") if not suppress_log:
logger.error(f"请求接口 {url} 失败,错误码:{code} {msg}")
return Result(code, msg) return Result(code, msg)
return Result(0, msg, resp.get("data")) return Result(0, msg, resp.get("data"))
else: elif not suppress_log:
logger.error(f"请求接口 {api_path} 失败") logger.error(f"请求接口 {url} 失败")
except Exception as e: except Exception as e:
logger.error(f"请求接口 {api_path} 异常:" + str(e)) if not suppress_log:
logger.error(f"请求接口 {url} 异常:" + str(e))
return None return None
+64 -17
View File
@@ -32,15 +32,52 @@ class TrimeMedia:
if not host or not username or not password: if not host or not username or not password:
logger.error("飞牛影视配置不完整!!") logger.error("飞牛影视配置不完整!!")
return return
host = UrlUtils.standardize_base_url(host).rstrip("/")
if play_host:
self._playhost = UrlUtils.standardize_base_url(play_host).rstrip("/")
self._username = username self._username = username
self._password = password self._password = password
self._sync_libraries = sync_libraries or [] self._sync_libraries = sync_libraries or []
self._api = fnapi.Api(host, apikey="16CCEB3D-AB42-077D-36A1-F355324E4237")
if (api := self.__create_api(host)) is None:
logger.error(f"请检查服务端地址 {host}")
return
self._api = api
if play_api := self.__create_api(play_host):
self._playhost = play_api.host
elif play_host:
logger.warning(f"请检查外网播放地址 {play_host}")
self.reconnect() self.reconnect()
@property
def api(self) -> Optional[fnapi.Api]:
"""
获得飞牛API
"""
return self._api
def __create_api(self, host: Optional[str]) -> Optional[fnapi.Api]:
"""
创建一个飞牛API
:param host: 服务端地址
:return: 如果地址无效、不可访问则返回None
"""
if not host:
return None
api_key = "16CCEB3D-AB42-077D-36A1-F355324E4237"
host = UrlUtils.standardize_base_url(host).rstrip("/")
if not host.endswith("/v"):
# 尝试补上结尾的/v 测试能否正常访问
api = fnapi.Api(host + "/v", api_key)
if api.sys_version():
return api
# 测试用户配置的地址
api = fnapi.Api(host, api_key)
return api if api.sys_version() else None
def __del__(self):
self.disconnect()
def is_configured(self) -> bool: def is_configured(self) -> bool:
return self._api is not None return self._api is not None
@@ -62,14 +99,27 @@ class TrimeMedia:
""" """
if not self.is_configured(): if not self.is_configured():
return False return False
if (fnver := self._api.sys_version()) is None:
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()
if self._userinfo is None: if self._userinfo is None:
return False return False
logger.debug(f"{self._userinfo.username} 成功登录飞牛影视") logger.debug(f"{self._username} 成功登录飞牛影视")
return True return True
def disconnect(self):
"""
断开与飞牛的连接
"""
if self.is_authenticated():
self._api.logout()
self._userinfo = None
logger.debug(f"{self._username} 已断开飞牛影视")
def get_librarys( def get_librarys(
self, hidden: Optional[bool] = False self, hidden: Optional[bool] = False
) -> List[schemas.MediaServerLibrary]: ) -> List[schemas.MediaServerLibrary]:
@@ -107,7 +157,7 @@ class TrimeMedia:
f"{self._api.host}{img_path}?w=256" f"{self._api.host}{img_path}?w=256"
for img_path in library.posters or [] for img_path in library.posters or []
], ],
link=f"{self._playhost or self._api.host}/v/library/{library.guid}", link=f"{self._playhost or self._api.host}/library/{library.guid}",
) )
) )
return libraries return libraries
@@ -280,7 +330,7 @@ class TrimeMedia:
lib = self.__match_library_by_path(item.target_path) lib = self.__match_library_by_path(item.target_path)
if lib is None: if lib is None:
# 如果有匹配失败的,刷新整个库 # 如果有匹配失败的,刷新整个库
return self._api.mdb_scanall() return self.refresh_root_library()
# 媒体库去重 # 媒体库去重
libraries.add(lib.guid) libraries.add(lib.guid)
@@ -290,7 +340,7 @@ class TrimeMedia:
logger.info(f"刷新媒体库:{lib.name}") logger.info(f"刷新媒体库:{lib.name}")
if not self._api.mdb_scan(lib): if not self._api.mdb_scan(lib):
# 如果失败,刷新整个库 # 如果失败,刷新整个库
return self._api.mdb_scanall() return self.refresh_root_library()
return True return True
def __match_library_by_path(self, path: Path) -> Optional[fnapi.MediaDb]: def __match_library_by_path(self, path: Path) -> Optional[fnapi.MediaDb]:
@@ -336,7 +386,7 @@ class TrimeMedia:
if item.watched: if item.watched:
user_state.played = True user_state.played = True
if item.duration and item.ts is not None: if item.duration and item.ts is not None:
user_state.percentage = item.ts / item.duration user_state.percentage = item.ts / item.duration * 100
user_state.resume = True user_state.resume = True
if item.type is None: if item.type is None:
item_type = None item_type = None
@@ -362,23 +412,20 @@ class TrimeMedia:
拼装播放链接 拼装播放链接
""" """
if item.type == fnapi.Type.EPISODE: if item.type == fnapi.Type.EPISODE:
return f"{host}/v/tv/episode/{item.guid}" return f"{host}/tv/episode/{item.guid}"
elif item.type == fnapi.Type.SEASON: elif item.type == fnapi.Type.SEASON:
return f"{host}/v/tv/season/{item.guid}" return f"{host}/tv/season/{item.guid}"
elif item.type == fnapi.Type.MOVIE: elif item.type == fnapi.Type.MOVIE:
return f"{host}/v/movie/{item.guid}" return f"{host}/movie/{item.guid}"
elif item.type == fnapi.Type.TV: elif item.type == fnapi.Type.TV:
return f"{host}/v/tv/{item.guid}" return f"{host}/tv/{item.guid}"
else: else:
# 其它类型走通用页面,由飞牛来判断 # 其它类型走通用页面,由飞牛来判断
return f"{host}/v/other/{item.guid}" return f"{host}/other/{item.guid}"
def __build_media_server_play_item( def __build_media_server_play_item(
self, item: fnapi.Item self, item: fnapi.Item
) -> schemas.MediaServerPlayItem: ) -> schemas.MediaServerPlayItem:
"""
:params use_backdrop: 是否优先使用Backdrop类型的图片
"""
if item.type == fnapi.Type.EPISODE: if item.type == fnapi.Type.EPISODE:
title = item.tv_title title = item.tv_title
subtitle = f"S{item.season_number}:{item.episode_number} - {item.title}" subtitle = f"S{item.season_number}:{item.episode_number} - {item.title}"