From c4d3d28491d82cbcc2ff0eced7569b6e71874b51 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 10 May 2026 12:19:36 +0800 Subject: [PATCH] fix: avoid blocking Ugreen startup on library preload Delay Ugreen library loading until it is needed and cap poster wall pagination so a single Ugreen server cannot hang backend startup.\n\nFixes #5740 --- app/modules/ugreen/ugreen.py | 17 ++++-- tests/test_ugreen_mediaserver.py | 91 ++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/app/modules/ugreen/ugreen.py b/app/modules/ugreen/ugreen.py index 7911915d..e736280f 100644 --- a/app/modules/ugreen/ugreen.py +++ b/app/modules/ugreen/ugreen.py @@ -15,6 +15,8 @@ from app.utils.url import UrlUtils class Ugreen: + LIBRARY_PATH_PAGE_LIMIT = 200 + _username: Optional[str] = None _password: Optional[str] = None @@ -171,11 +173,13 @@ class Ugreen: if not self.is_configured(): return False + self._libraries = {} + self._library_paths = {} + # 关闭旧连接(不主动登出,避免破坏可复用会话) self.disconnect(logout=False) if self.__restore_persisted_session(): - self.get_librarys() return True self._api = Api(host=self._host, verify_ssl=self._verify_ssl) @@ -191,7 +195,6 @@ class Ugreen: # 登录成功后持久化参数,下次优先复用 self.__save_persisted_session() logger.debug(f"{self._username} 成功登录绿联影视") - self.get_librarys() return True def disconnect(self, logout: bool = False): @@ -204,6 +207,8 @@ class Ugreen: self._api = None self._userinfo = None logger.debug(f"{self._username} 已断开绿联影视") + self._libraries = {} + self._library_paths = {} @staticmethod def __normalize_dir_path(path: Union[str, Path, None]) -> str: @@ -487,7 +492,7 @@ class Ugreen: paths: dict[str, str] = {} page = 1 - while True: + while page <= self.LIBRARY_PATH_PAGE_LIMIT: data = self._api.poster_wall_get_folder(page=page, page_size=100) if not data: break @@ -502,6 +507,12 @@ class Ugreen: break page += 1 + if page > self.LIBRARY_PATH_PAGE_LIMIT: + # 部分固件分页标志异常时会无限返回下一页,这里加硬限制避免阻塞调用方。 + logger.warning( + f"绿联影视 {self._username} 媒体库目录分页超过上限 {self.LIBRARY_PATH_PAGE_LIMIT} 页,停止继续加载" + ) + return paths def get_librarys(self, hidden: Optional[bool] = False) -> List[schemas.MediaServerLibrary]: diff --git a/tests/test_ugreen_mediaserver.py b/tests/test_ugreen_mediaserver.py index 5bed79de..b48cac2c 100644 --- a/tests/test_ugreen_mediaserver.py +++ b/tests/test_ugreen_mediaserver.py @@ -101,6 +101,50 @@ class _FakeUgreenApi: return {"total_num": 0} +class _FakeReconnectApi: + token = "test-token" + + @staticmethod + def login(_username, _password): + return "test-token" + + @staticmethod + def current_user(): + return {"name": "tester"} + + @staticmethod + def close(): + return None + + @staticmethod + def export_session_state(): + return {"token": "test-token", "public_key": "public-key"} + + +class _PagedFolderApi: + def __init__(self, stop_after: int | None = None): + self.calls = 0 + self.pages = [] + self.stop_after = stop_after + + def poster_wall_get_folder(self, page: int, page_size: int = 100): + self.calls += 1 + self.pages.append(page) + if self.stop_after is not None and page >= self.stop_after: + return { + "folder_arr": [ + {"media_lib_set_id": page, "path": f"/library/{page}"}, + ], + "is_last_page": True, + } + return { + "folder_arr": [ + {"media_lib_set_id": page, "path": f"/library/{page}"}, + ], + "is_last_page": False, + } + + class UgreenScanModeTest(unittest.TestCase): def test_resolve_scan_type(self): resolve = Ugreen._Ugreen__resolve_scan_type @@ -148,6 +192,53 @@ class UgreenStatisticTest(unittest.TestCase): self.assertIsNone(stat.episode_count) +class UgreenReconnectTest(unittest.TestCase): + def test_reconnect_does_not_eagerly_load_libraries(self): + ugreen = Ugreen.__new__(Ugreen) + ugreen._host = "http://127.0.0.1:9999" + ugreen._username = "tester" + ugreen._password = "secret" + ugreen._verify_ssl = True + ugreen._libraries = {"old": {"id": "old"}} + ugreen._library_paths = {"old": "/old"} + ugreen._api = None + ugreen._userinfo = None + + with patch.object(Ugreen, "_Ugreen__restore_persisted_session", return_value=False), patch( + "_test_ugreen_module.Api", return_value=_FakeReconnectApi() + ), patch.object(Ugreen, "_Ugreen__save_persisted_session", return_value=None), patch.object( + Ugreen, "disconnect", wraps=ugreen.disconnect + ), patch.object(Ugreen, "get_librarys") as mocked_get_librarys: + self.assertTrue(ugreen.reconnect()) + + mocked_get_librarys.assert_not_called() + self.assertEqual(ugreen._libraries, {}) + self.assertEqual(ugreen._library_paths, {}) + + +class UgreenLibraryPathLimitTest(unittest.TestCase): + def test_load_library_paths_stops_at_last_page(self): + ugreen = Ugreen.__new__(Ugreen) + ugreen._username = "tester" + ugreen._api = _PagedFolderApi(stop_after=3) + + paths = ugreen._Ugreen__load_library_paths() + + self.assertEqual(ugreen._api.pages, [1, 2, 3]) + self.assertEqual(paths["3"], "/library/3") + + def test_load_library_paths_respects_page_limit(self): + ugreen = Ugreen.__new__(Ugreen) + ugreen._username = "tester" + ugreen._api = _PagedFolderApi() + + paths = ugreen._Ugreen__load_library_paths() + + self.assertEqual(ugreen._api.calls, Ugreen.LIBRARY_PATH_PAGE_LIMIT) + self.assertEqual(len(paths), Ugreen.LIBRARY_PATH_PAGE_LIMIT) + self.assertIn(str(Ugreen.LIBRARY_PATH_PAGE_LIMIT), paths) + + class DashboardStatisticTest(unittest.TestCase): @unittest.skipIf(dashboard_endpoint is None, "dashboard endpoint dependencies are missing") def test_statistic_all_episode_missing(self):