From d0b1348c965e5d42e6c66be0bc2a5081a3aef2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=AF=E5=A4=A7=E4=BE=A0?= Date: Sat, 29 Mar 2025 11:28:26 +0800 Subject: [PATCH 1/3] fix some warnings --- app/modules/trimemedia/__init__.py | 2 +- app/modules/trimemedia/api.py | 82 ++++++++++++++++------------ app/modules/trimemedia/trimemedia.py | 42 +++++++------- 3 files changed, 68 insertions(+), 58 deletions(-) diff --git a/app/modules/trimemedia/__init__.py b/app/modules/trimemedia/__init__.py index 99670e7b..a19ee4bc 100644 --- a/app/modules/trimemedia/__init__.py +++ b/app/modules/trimemedia/__init__.py @@ -73,7 +73,7 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]): for name, server in self.get_instances().items(): if not server.is_configured(): return False, f"飞牛影视配置不完整:{name}" - if server.is_inactive() and server.reconnect() != True: + if server.is_inactive() and not server.reconnect(): return False, f"无法连接飞牛影视:{name}" return True, "" diff --git a/app/modules/trimemedia/api.py b/app/modules/trimemedia/api.py index 42c47df4..5afeb83d 100644 --- a/app/modules/trimemedia/api.py +++ b/app/modules/trimemedia/api.py @@ -4,7 +4,7 @@ import random import time from dataclasses import dataclass from enum import Enum -from typing import Optional, Union, List +from typing import List, Optional, Union from app.core.config import settings from app.log import logger @@ -19,27 +19,27 @@ class User: class Category(Enum): - Movie = "Movie" + MOVIE = "Movie" TV = "TV" - Mix = "Mix" - Others = "Others" + MIX = "Mix" + OTHERS = "Others" @classmethod def _missing_(cls, value): - return cls.Others + return cls.OTHERS class Type(Enum): - Movie = "Movie" + MOVIE = "Movie" TV = "TV" - Season = "Season" - Episode = "Episode" - Video = "Video" - Directory = "Directory" + SEASON = "Season" + EPISODE = "Episode" + VIDEO = "Video" + DIRECTORY = "Directory" @classmethod def _missing_(cls, value): - return cls.Video + return cls.VIDEO @dataclass @@ -131,14 +131,14 @@ class Api: :return: 成功返回token 否则返回None """ if ( - res := self.__request_api( - "/login", - data={ - "username": username, - "password": password, - "app_name": "trimemedia-web", - }, - ) + res := self.__request_api( + "/login", + data={ + "username": username, + "password": password, + "app_name": "trimemedia-web", + }, + ) ) and res.success: self._token = res.data.get("token") return self._token @@ -250,7 +250,7 @@ class Api: 扫描指定媒体库 """ if ( - res := self.__request_api(f"/mdb/scan/{mdb.guid}", data={}) + res := self.__request_api(f"/mdb/scan/{mdb.guid}", data={}) ) and res.success: if res.data: return True @@ -272,22 +272,22 @@ class Api: return item def item_list( - self, - guid: Optional[str] = None, - type=None, - exclude_grouped_video=True, - page=1, - page_size=22, - sort_by="create_time", - sort="DESC", + self, + guid: Optional[str] = None, + types=None, + exclude_grouped_video=True, + page=1, + page_size=22, + sort_by="create_time", + sort="DESC", ) -> Optional[list[Item]]: """ 媒体列表 """ - if type is None: - type = [Type.Movie, Type.TV, Type.Directory, Type.Video] + if types is None: + types = [Type.MOVIE, Type.TV, Type.DIRECTORY, Type.VIDEO] post = { - "tags": {"type": type} if type else {}, + "tags": {"type": types} if types else {}, "sort_type": sort, "sort_column": sort_by, "page": page, @@ -307,25 +307,31 @@ class Api: 搜索影片、演员 """ if ( - res := self.__request_api("/search/list", params={"q": keywords}) + res := self.__request_api("/search/list", params={"q": keywords}) ) and res.success: return [self.__build_item(info) for info in res.data] return None def item(self, guid: str) -> Optional[Item]: - """ """ + """ + 查询媒体详情 + """ if (res := self.__request_api(f"/item/{guid}")) and res.success: return self.__build_item(res.data) return None def season_list(self, tv_guid: str) -> Optional[list[Item]]: - """ """ + """ + 查询季列表 + """ if (res := self.__request_api(f"/season/list/{tv_guid}")) and res.success: return [self.__build_item(info) for info in res.data] return None def episode_list(self, season_guid: str) -> Optional[list[Item]]: - """ """ + """ + 查询剧集列表 + """ if (res := self.__request_api(f"/episode/list/{season_guid}")) and res.success: return [self.__build_item(info) for info in res.data] return None @@ -366,7 +372,11 @@ class Api: return f"nonce={nonce}×tamp={ts}&sign={sign}" def __request_api( - self, api: str, method: str = None, params: dict = None, data: dict = None + self, + api: str, + method: Optional[str] = None, + params: Optional[dict] = None, + data: Optional[dict] = None, ): """ 请求飞牛影视API diff --git a/app/modules/trimemedia/trimemedia.py b/app/modules/trimemedia/trimemedia.py index 8a21d7d8..8014eb2c 100644 --- a/app/modules/trimemedia/trimemedia.py +++ b/app/modules/trimemedia/trimemedia.py @@ -26,7 +26,7 @@ class TrimeMedia: username: Optional[str] = None, password: Optional[str] = None, play_host: Optional[str] = None, - sync_libraries: list = None, + sync_libraries: Optional[list] = None, **kwargs, ): if not host or not username or not password: @@ -87,11 +87,11 @@ class TrimeMedia: for library in self._libraries.values(): if hidden and self.__is_library_blocked(library.guid): continue - if library.category == fnapi.Category.Movie: + if library.category == fnapi.Category.MOVIE: library_type = MediaType.MOVIE.value elif library.category == fnapi.Category.TV: library_type = MediaType.TV.value - elif library.category == fnapi.Category.Others: + elif library.category == fnapi.Category.OTHERS: # 忽略这个库 continue else: @@ -170,7 +170,7 @@ class TrimeMedia: movies = [] items = self._api.search_list(keywords=title) or [] for item in items: - if item.type != fnapi.Type.Movie: + if item.type != fnapi.Type.MOVIE: continue if ( (not tmdb_id or tmdb_id == item.tmdb_id) @@ -361,11 +361,11 @@ class TrimeMedia: """ 拼装播放链接 """ - if item.type == fnapi.Type.Episode: + if item.type == fnapi.Type.EPISODE: return f"{host}/v/tv/episode/{item.guid}" - elif item.type == fnapi.Type.Season: + elif item.type == fnapi.Type.SEASON: return f"{host}/v/tv/season/{item.guid}" - elif item.type == fnapi.Type.Movie: + elif item.type == fnapi.Type.MOVIE: return f"{host}/v/movie/{item.guid}" elif item.type == fnapi.Type.TV: return f"{host}/v/tv/{item.guid}" @@ -379,22 +379,22 @@ class TrimeMedia: """ :params use_backdrop: 是否优先使用Backdrop类型的图片 """ - if item.type == fnapi.Type.Episode: + if item.type == fnapi.Type.EPISODE: title = item.tv_title subtitle = f"S{item.season_number}:{item.episode_number} - {item.title}" else: title = item.title - subtitle = "电影" if item.type == fnapi.Type.Movie else "视频" - type = ( + subtitle = "电影" if item.type == fnapi.Type.MOVIE else "视频" + types = ( MediaType.MOVIE.value - if item.type in [fnapi.Type.Movie, fnapi.Type.Video] + if item.type in [fnapi.Type.MOVIE, fnapi.Type.VIDEO] else MediaType.TV.value ) return schemas.MediaServerPlayItem( id=item.guid, title=title, subtitle=subtitle, - type=type, + type=types, image=f"{self._api.host}{item.poster}", link=self.__build_play_url(self._playhost or self._api.host, item), percent=( @@ -421,22 +421,22 @@ class TrimeMedia: """ if not self.is_authenticated(): return None - if (SIZE := limit) is None: - SIZE = -1 + if (page_size := limit) is None: + page_size = -1 items = ( self._api.item_list( guid=parent, page=start_index + 1, - page_size=SIZE, - type=[fnapi.Type.Movie, fnapi.Type.TV, fnapi.Type.Directory], + page_size=page_size, + types=[fnapi.Type.MOVIE, fnapi.Type.TV, fnapi.Type.DIRECTORY], ) or [] ) for item in items: - if item.type == fnapi.Type.Directory: + if item.type == fnapi.Type.DIRECTORY: for items in self.get_items(parent=item.guid): yield items - elif item.type in [fnapi.Type.Movie, fnapi.Type.TV]: + elif item.type in [fnapi.Type.MOVIE, fnapi.Type.TV]: yield self.__build_media_server_item(item) return None @@ -482,7 +482,7 @@ class TrimeMedia: self._api.item_list( page=1, page_size=max(100, num * 5), - type=[fnapi.Type.Movie, fnapi.Type.TV], + types=[fnapi.Type.MOVIE, fnapi.Type.TV], ) or [] ) @@ -505,7 +505,7 @@ class TrimeMedia: self._api.item_list( page=1, page_size=max(100, num * 5), - type=[fnapi.Type.Movie, fnapi.Type.TV], + types=[fnapi.Type.MOVIE, fnapi.Type.TV], ) or [] ) @@ -534,7 +534,7 @@ class TrimeMedia: def __is_library_blocked(self, library_guid: str): if library := self._libraries.get(library_guid): - if library.category == fnapi.Category.Others: + if library.category == fnapi.Category.OTHERS: # 忽略这个库 return True return ( From 8d39cc87f70cb8357efa4c01069cf8ff61168643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=AF=E5=A4=A7=E4=BE=A0?= Date: Sat, 29 Mar 2025 14:03:16 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=8F=90=E5=8D=87=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=E5=9C=B0=E5=9D=80=E7=9A=84=E5=85=BC=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modules/trimemedia/__init__.py | 4 +- app/modules/trimemedia/api.py | 52 ++++++++++++++---- app/modules/trimemedia/trimemedia.py | 81 ++++++++++++++++++++++------ 3 files changed, 110 insertions(+), 27 deletions(-) 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}" From 33fc3fd63bdbcf4d4e48ff874a9c31e1dc497a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=AF=E5=A4=A7=E4=BE=A0?= Date: Mon, 7 Apr 2025 17:20:47 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=AA=92=E4=BD=93=E7=9A=84api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modules/trimemedia/api.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/modules/trimemedia/api.py b/app/modules/trimemedia/api.py index a88a201c..d4c8744e 100644 --- a/app/modules/trimemedia/api.py +++ b/app/modules/trimemedia/api.py @@ -349,6 +349,23 @@ class Api: return self.__build_item(res.data) return None + def del_item(self, guid: str, delete_file: bool) -> bool: + """ + 删除媒体 + + :param delete_file: True删除媒体文件,False仅从媒体库移除 + """ + if ( + res := self.__request_api( + f"/item/{guid}", + method="delete", + data={"delete_file": 1 if delete_file else 0, "media_guids": []}, + ) + ) and res.success: + if res.data: + return True + return False + def season_list(self, tv_guid: str) -> Optional[list[Item]]: """ 查询季列表 @@ -439,7 +456,7 @@ class Api: url = self._host + api_path if method is None: method = "get" if data is None else "post" - if method == "post": + if method != "get": json_body = ( json.dumps(data, allow_nan=False, cls=JsonEncoder) if data else "" )