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.
This commit is contained in:
edde746
2026-09-03 11:36:29 +02:00
parent 7e84df6385
commit 1373c299df
3 changed files with 27 additions and 36 deletions
+15 -35
View File
@@ -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<List<PlexHomeUser>> Function(String accountToken)? plexHomeUserFetcher,
required this.plexHome,
Future<Map<String, dynamic>> 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<List<PlexHomeUser>> Function(String accountToken) _plexHomeUserFetcher;
final PlexHomeService plexHome;
final Future<Map<String, dynamic>> 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<bool> _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<PlexHomeUser>? _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<List<PlexHomeUser>> _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<void> _migrateLegacyPlexHomeUsersCacheForExistingAccount(PlexAccountConnection? account) async {
if (storage.prefs.getString('home_users_cache') == null) return;
var target = account;
+1
View File
@@ -2032,6 +2032,7 @@ class _SetupScreenState extends State<SetupScreen> with MountedSetStateMixin {
connectionRegistry: connRegistry,
serverRegistry: registry,
profileRegistry: profileRegistry,
plexHome: context.read<PlexHomeService>(),
);
await bootstrap.run();
final pruned = await ProfileConnectionCleanup(
+11 -1
View File
@@ -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);
});
});
}