diff --git a/app/modules/trimemedia/__init__.py b/app/modules/trimemedia/__init__.py index a19ee4bc..be12ec18 100644 --- a/app/modules/trimemedia/__init__.py +++ b/app/modules/trimemedia/__init__.py @@ -62,7 +62,9 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]): server.reconnect() def stop(self): - pass + for server in self.get_instances().values(): + if server.is_authenticated(): + server.disconnect() def test(self) -> Optional[Tuple[bool, str]]: """ diff --git a/app/modules/trimemedia/api.py b/app/modules/trimemedia/api.py index 5afeb83d..a88a201c 100644 --- a/app/modules/trimemedia/api.py +++ b/app/modules/trimemedia/api.py @@ -60,6 +60,13 @@ class MediaDbSummary: total: int = 0 +@dataclass +class Version: + # 飞牛影视版本 + frontend: Optional[str] = None + backend: Optional[str] = None + + @dataclass class Item: guid: str @@ -103,6 +110,7 @@ class Api: "_apikey", "_api_path", "_request_utils", + "_version", ) @property @@ -117,13 +125,34 @@ class Api: def apikey(self) -> str: return self._apikey + @property + def version(self) -> Optional[Version]: + return self._version + 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._apikey = apikey - self._token = None + self._token: Optional[str] = None + self._version: Optional[Version] = None 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]: """ 登录飞牛影视 @@ -344,12 +373,12 @@ class Api: return [self.__build_item(info) for info in res.data] 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] != "/": - api_path = "/" + api_path + if not api_path.startswith("/v"): + api_path = "/v" + api_path nonce = str(random.randint(100000, 999999)) ts = str(int(time.time() * 1000)) md5 = hashlib.md5() @@ -377,9 +406,12 @@ class Api: method: Optional[str] = None, params: Optional[dict] = None, data: Optional[dict] = None, + suppress_log=False, ): """ 请求飞牛影视API + + :param suppress_log: 是否禁止日志 """ @dataclass @@ -432,11 +464,13 @@ class Api: resp = res.json() msg = resp.get("msg") 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(0, msg, resp.get("data")) - else: - logger.error(f"请求接口 {api_path} 失败") + elif not suppress_log: + logger.error(f"请求接口 {url} 失败") except Exception as e: - logger.error(f"请求接口 {api_path} 异常:" + str(e)) + if not suppress_log: + logger.error(f"请求接口 {url} 异常:" + str(e)) return None diff --git a/app/modules/trimemedia/trimemedia.py b/app/modules/trimemedia/trimemedia.py index 8014eb2c..58603d6e 100644 --- a/app/modules/trimemedia/trimemedia.py +++ b/app/modules/trimemedia/trimemedia.py @@ -32,15 +32,52 @@ class TrimeMedia: if not host or not username or not password: logger.error("飞牛影视配置不完整!!") return - host = UrlUtils.standardize_base_url(host).rstrip("/") - if play_host: - self._playhost = UrlUtils.standardize_base_url(play_host).rstrip("/") self._username = username self._password = password 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() + @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: return self._api is not None @@ -62,14 +99,27 @@ class TrimeMedia: """ if not self.is_configured(): 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: return False self._userinfo = self._api.user_info() if self._userinfo is None: return False - logger.debug(f"{self._userinfo.username} 成功登录飞牛影视") + logger.debug(f"{self._username} 成功登录飞牛影视") return True + def disconnect(self): + """ + 断开与飞牛的连接 + """ + if self.is_authenticated(): + self._api.logout() + self._userinfo = None + logger.debug(f"{self._username} 已断开飞牛影视") + def get_librarys( self, hidden: Optional[bool] = False ) -> List[schemas.MediaServerLibrary]: @@ -107,7 +157,7 @@ class TrimeMedia: f"{self._api.host}{img_path}?w=256" 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 @@ -280,7 +330,7 @@ class TrimeMedia: lib = self.__match_library_by_path(item.target_path) if lib is None: # 如果有匹配失败的,刷新整个库 - return self._api.mdb_scanall() + return self.refresh_root_library() # 媒体库去重 libraries.add(lib.guid) @@ -290,7 +340,7 @@ class TrimeMedia: logger.info(f"刷新媒体库:{lib.name}") if not self._api.mdb_scan(lib): # 如果失败,刷新整个库 - return self._api.mdb_scanall() + return self.refresh_root_library() return True def __match_library_by_path(self, path: Path) -> Optional[fnapi.MediaDb]: @@ -336,7 +386,7 @@ class TrimeMedia: if item.watched: user_state.played = True 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 if item.type is None: item_type = None @@ -362,23 +412,20 @@ class TrimeMedia: 拼装播放链接 """ 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: - return f"{host}/v/tv/season/{item.guid}" + return f"{host}/tv/season/{item.guid}" 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: - return f"{host}/v/tv/{item.guid}" + return f"{host}/tv/{item.guid}" else: # 其它类型走通用页面,由飞牛来判断 - return f"{host}/v/other/{item.guid}" + return f"{host}/other/{item.guid}" def __build_media_server_play_item( self, item: fnapi.Item ) -> schemas.MediaServerPlayItem: - """ - :params use_backdrop: 是否优先使用Backdrop类型的图片 - """ if item.type == fnapi.Type.EPISODE: title = item.tv_title subtitle = f"S{item.season_number}:{item.episode_number} - {item.title}"