From 1373c299df983a3a3bac99f49a282bd5ea037cd1 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:14:28 +0200 Subject: [PATCH] chore(plex): hydrate Plex Home users at bootstrap through PlexHomeService ConnectionBootstrap kept a private cache read/fetch/write path for Plex Home users beside PlexHomeService, which already owns refresh coalescing, generation checks, durable writes, and publication. The bootstrap now keeps only the one-time legacy cache migration, then reloads the service from storage or asks it to refresh, and reads the hydrated users from the service snapshot. A failed hydration clears the service's empty cache slot before the migrated account is removed. --- lib/connection/connection_bootstrap.dart | 50 ++++++------------- lib/main.dart | 1 + .../connection/connection_bootstrap_test.dart | 12 ++++- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/lib/connection/connection_bootstrap.dart b/lib/connection/connection_bootstrap.dart index cae0191d1..2e06f7027 100644 --- a/lib/connection/connection_bootstrap.dart +++ b/lib/connection/connection_bootstrap.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import '../models/plex/plex_home.dart'; import '../models/plex/plex_home_user.dart'; +import '../profiles/plex_home_service.dart'; import '../profiles/profile.dart'; -import '../profiles/plex_home_cache_codec.dart'; import '../profiles/profile_registry.dart'; import '../services/plex_auth_service.dart'; import '../services/server_registry.dart'; @@ -20,24 +20,23 @@ import 'plex_account_setup.dart'; /// /// Plex Home users are NOT persisted here — the bootstrap copies the /// legacy `homeUsersCache` into the per-connection -/// `plex_home_users_{connectionId}` SharedPreferences slot so -/// [PlexHomeService] picks it up on cold start. +/// `plex_home_users_{connectionId}` SharedPreferences slot and asks +/// [PlexHomeService] to reload (or fetch) it. class ConnectionBootstrap { ConnectionBootstrap({ required this.storage, required this.connectionRegistry, required this.serverRegistry, required this.profileRegistry, - Future> Function(String accountToken)? plexHomeUserFetcher, + required this.plexHome, Future> Function(String accountToken)? plexUserInfoFetcher, - }) : _plexHomeUserFetcher = plexHomeUserFetcher ?? fetchPlexHomeUsers, - _plexUserInfoFetcher = plexUserInfoFetcher ?? _fetchPlexUserInfo; + }) : _plexUserInfoFetcher = plexUserInfoFetcher ?? _fetchPlexUserInfo; final StorageService storage; final ConnectionRegistry connectionRegistry; final ServerRegistry serverRegistry; final ProfileRegistry profileRegistry; - final Future> Function(String accountToken) _plexHomeUserFetcher; + final PlexHomeService plexHome; final Future> Function(String accountToken) _plexUserInfoFetcher; static const String _keyProfileMigrationV1Done = 'profile_migration_v1_done'; @@ -63,6 +62,9 @@ class ConnectionBootstrap { final prepared = await _preparePlexVirtualProfile(account); if (!prepared) { if (migratedAccount != null && hadLegacyPlexToken) { + // A failed/empty fetch may have left an empty cache slot on disk + // for the id we are about to drop; don't leave it orphaned. + await storage.clearPlexHomeUsersCache(migratedAccount.id); await connectionRegistry.remove(migratedAccount.id); } appLogger.w('Migration: could not hydrate Plex Home profiles for ${account.id}; will retry later'); @@ -147,9 +149,12 @@ class ConnectionBootstrap { /// profile. Plex users are never persisted as local Plezy profiles. Future _preparePlexVirtualProfile(PlexAccountConnection account) async { final copied = await _migrateLegacyPlexHomeUsersCache(account.id); - var users = copied ? _readPlexHomeUsersCache(account.id) : null; - users ??= await _fetchAndCachePlexHomeUsers(account); - final hydratedUsers = users; + if (copied) { + await plexHome.reloadFromStorage(); + } else { + await plexHome.refresh(account); + } + final hydratedUsers = plexHome.current[account.id] ?? const []; if (hydratedUsers.isEmpty) return false; final legacyActiveUuid = storage.getCurrentUserUUID(); @@ -196,31 +201,6 @@ class ConnectionBootstrap { } } - List? _readPlexHomeUsersCache(String connectionId) { - final raw = storage.getPlexHomeUsersCacheJson(connectionId); - if (raw == null || raw.isEmpty) return null; - try { - return decodePlexHomeUsersCache(raw); - } catch (e, st) { - appLogger.w('Migration: failed to read Plex Home cache for $connectionId', error: e, stackTrace: st); - return null; - } - } - - Future> _fetchAndCachePlexHomeUsers(PlexAccountConnection account) async { - try { - final users = await _plexHomeUserFetcher(account.accountToken); - if (users.isNotEmpty) { - await storage.savePlexHomeUsersCache(account.id, encodePlexHomeUsersCache(users)); - appLogger.i('Migration: fetched ${users.length} Plex Home users for ${account.id}'); - } - return users; - } catch (e, st) { - appLogger.w('Migration: Plex Home fetch failed for ${account.id}', error: e, stackTrace: st); - return const []; - } - } - Future _migrateLegacyPlexHomeUsersCacheForExistingAccount(PlexAccountConnection? account) async { if (storage.prefs.getString('home_users_cache') == null) return; var target = account; diff --git a/lib/main.dart b/lib/main.dart index 1cdf34ba4..805e9916b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2032,6 +2032,7 @@ class _SetupScreenState extends State with MountedSetStateMixin { connectionRegistry: connRegistry, serverRegistry: registry, profileRegistry: profileRegistry, + plexHome: context.read(), ); await bootstrap.run(); final pruned = await ProfileConnectionCleanup( diff --git a/test/connection/connection_bootstrap_test.dart b/test/connection/connection_bootstrap_test.dart index f0c089f93..387458ab4 100644 --- a/test/connection/connection_bootstrap_test.dart +++ b/test/connection/connection_bootstrap_test.dart @@ -7,6 +7,8 @@ import 'package:plezy/connection/connection_bootstrap.dart'; import 'package:plezy/connection/connection_registry.dart'; import 'package:plezy/database/app_database.dart'; import 'package:plezy/models/plex/plex_home_user.dart'; +import 'package:plezy/profiles/plex_home_service.dart'; +import 'package:plezy/profiles/profile_connection_registry.dart'; import 'package:plezy/profiles/profile_registry.dart'; import 'package:plezy/services/server_registry.dart'; import 'package:plezy/services/storage_service.dart'; @@ -32,12 +34,19 @@ void main() { serverRegistry = ServerRegistry(storage); fetchedHomeUsers = const []; fetchedUserInfo = null; + final plexHome = PlexHomeService( + connections: registry, + profileConnections: ProfileConnectionRegistry(db), + storage: storage, + plexHomeUserFetcher: (_) async => fetchedHomeUsers, + ); + addTearDown(plexHome.dispose); bootstrap = ConnectionBootstrap( storage: storage, connectionRegistry: registry, serverRegistry: serverRegistry, profileRegistry: profileRegistry, - plexHomeUserFetcher: (_) async => fetchedHomeUsers, + plexHome: plexHome, plexUserInfoFetcher: (_) async => fetchedUserInfo ?? (throw StateError('user info unavailable')), ); }); @@ -226,6 +235,7 @@ void main() { expect(storage.getActiveProfileId(), isNull); expect(await registry.list(), isEmpty); expect(await profileRegistry.list(), isEmpty); + expect(storage.getPlexHomeUsersCacheJson('plex.client-owner'), isNull); }); }); }