fix(profiles): parse real Plex Home uuids + migrate pref scopes
This commit is contained in:
@@ -211,11 +211,16 @@ String plexHomeProfileId({required String accountConnectionId, required String h
|
||||
return 'plex-home-$accountConnectionId-$homeUserUuid';
|
||||
}
|
||||
|
||||
/// Anchor on the trailing 36-char UUID — both `accountConnectionId` and
|
||||
/// `homeUserUuid` may contain hyphens, so a `lastIndexOf('-')` would slice
|
||||
/// inside the UUID itself.
|
||||
/// Anchor on the trailing home-user uuid — `accountConnectionId` may contain
|
||||
/// hyphens, so a `lastIndexOf('-')` would slice inside a dashed uuid. The
|
||||
/// `$` anchor resolves ambiguous hyphens: an earlier candidate split only
|
||||
/// wins if the remainder is exactly one uuid.
|
||||
///
|
||||
/// Real plex.tv `/api/v2/home/users` uuids are 16-char plain hex
|
||||
/// (e.g. `7b4af7a9f26254bd`); the 36-char dashed RFC-4122 shape is kept as a
|
||||
/// defensive alternate.
|
||||
final RegExp _trailingHomeUserUuidPattern = RegExp(
|
||||
r'-([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$',
|
||||
r'-([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9a-fA-F]{16})$',
|
||||
);
|
||||
|
||||
/// Inverse of [plexHomeProfileId]. Returns `null` if [id] doesn't match the
|
||||
|
||||
@@ -46,6 +46,43 @@ class StorageService extends BaseSharedPreferencesService {
|
||||
// migration ran (after migration the slot is empty so this is a no-op).
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
LogRedactionManager.registerToken(getPlexToken());
|
||||
await _migratePlexHomeUserScopes();
|
||||
}
|
||||
|
||||
/// One-time repair for prefs scoped by a full `plex-home-…` profile id.
|
||||
/// [parsePlexHomeProfileId] historically rejected real 16-hex home-user
|
||||
/// uuids, so `_userPrefix` fell back to the full profile id and wrote
|
||||
/// `user_plex-home-{acct}-{uuid}_*` keys instead of the intended
|
||||
/// `user_{uuid}_*`. Move them onto the uuid scope. On conflict the
|
||||
/// full-id value wins: it is the more recently written one (uuid-scoped
|
||||
/// keys can only predate the profiles migration).
|
||||
Future<void> _migratePlexHomeUserScopes() async {
|
||||
const scopePrefix = 'user_plex-home-';
|
||||
final keys = prefs.keys.where((k) => k.startsWith(scopePrefix)).toList(growable: false);
|
||||
for (final key in keys) {
|
||||
final withoutUserPrefix = key.substring('user_'.length);
|
||||
// Profile ids contain no underscores, so the first `_` ends the scope.
|
||||
final sep = withoutUserPrefix.indexOf('_');
|
||||
if (sep <= 0) continue;
|
||||
final parsed = parsePlexHomeProfileId(withoutUserPrefix.substring(0, sep));
|
||||
if (parsed == null) continue;
|
||||
final newKey = 'user_${parsed.homeUserUuid}_${withoutUserPrefix.substring(sep + 1)}';
|
||||
switch (prefs.get(key)) {
|
||||
case final List<Object?> v:
|
||||
await prefs.setStringList(newKey, v.cast<String>());
|
||||
case final String v:
|
||||
await prefs.setString(newKey, v);
|
||||
case final bool v:
|
||||
await prefs.setBool(newKey, v);
|
||||
case final int v:
|
||||
await prefs.setInt(newKey, v);
|
||||
case final double v:
|
||||
await prefs.setDouble(newKey, v);
|
||||
default:
|
||||
continue; // Unknown shape — leave the old key untouched.
|
||||
}
|
||||
await prefs.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// User-scoped storage for per-profile library settings
|
||||
|
||||
@@ -86,9 +86,21 @@ void main() {
|
||||
expect(plexHomeProfileId(accountConnectionId: 'plex.dev1', homeUserUuid: 'uuid-1'), 'plex-home-plex.dev1-uuid-1');
|
||||
});
|
||||
|
||||
test('parsePlexHomeProfileId round-trips a real hyphenated UUID', () {
|
||||
// Real Plex Home UUIDs are 36-char standard UUIDs (4 internal hyphens),
|
||||
// and accountConnectionId can carry hyphens too (e.g. plex.client-id).
|
||||
test('parsePlexHomeProfileId round-trips a real 16-hex uuid', () {
|
||||
// Real plex.tv home-user uuids are 16-char plain hex, and the account
|
||||
// connection id itself usually ends in one (plex.{accountUuid}).
|
||||
const acct = 'plex.e443d57860076fc3';
|
||||
const uuid = '379704d0c6601309';
|
||||
final id = plexHomeProfileId(accountConnectionId: acct, homeUserUuid: uuid);
|
||||
final parsed = parsePlexHomeProfileId(id);
|
||||
expect(parsed, isNotNull);
|
||||
expect(parsed!.accountConnectionId, acct);
|
||||
expect(parsed.homeUserUuid, uuid);
|
||||
});
|
||||
|
||||
test('parsePlexHomeProfileId round-trips a hyphenated RFC-4122 uuid', () {
|
||||
// Defensive alternate shape; accountConnectionId can carry hyphens too
|
||||
// (e.g. plex.client-id).
|
||||
const acct = 'plex.client-id-123';
|
||||
const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef0123456789';
|
||||
final id = plexHomeProfileId(accountConnectionId: acct, homeUserUuid: uuid);
|
||||
@@ -98,10 +110,24 @@ void main() {
|
||||
expect(parsed.homeUserUuid, uuid);
|
||||
});
|
||||
|
||||
test('parsePlexHomeProfileId splits at the last uuid-shaped segment', () {
|
||||
// A hex-ending account id must not steal the uuid slot: the $ anchor
|
||||
// forces the trailing segment to be the home-user uuid.
|
||||
const acct = 'acct-aaaaaaaaaaaaaaaa';
|
||||
const uuid = 'bbbbbbbbbbbbbbbb';
|
||||
final parsed = parsePlexHomeProfileId(plexHomeProfileId(accountConnectionId: acct, homeUserUuid: uuid));
|
||||
expect(parsed, isNotNull);
|
||||
expect(parsed!.accountConnectionId, acct);
|
||||
expect(parsed.homeUserUuid, uuid);
|
||||
});
|
||||
|
||||
test('parsePlexHomeProfileId rejects non-Plex-Home ids', () {
|
||||
expect(parsePlexHomeProfileId('local-1'), isNull);
|
||||
expect(parsePlexHomeProfileId('plex-home-only'), isNull);
|
||||
expect(parsePlexHomeProfileId('plex-home-acct-not-a-uuid'), isNull);
|
||||
// 15 and 17 hex chars are not uuids.
|
||||
expect(parsePlexHomeProfileId('plex-home-acct-aaaaaaaaaaaaaaa'), isNull);
|
||||
expect(parsePlexHomeProfileId('plex-home-acct-aaaaaaaaaaaaaaaaa'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -369,6 +369,55 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Plex Home user-scope migration (full profile id → home-user uuid)
|
||||
// ============================================================
|
||||
|
||||
group('migratePlexHomeUserScopes (onInit)', () {
|
||||
const fullId = 'plex-home-plex.e443d57860076fc3-379704d0c6601309';
|
||||
const uuid = '379704d0c6601309';
|
||||
|
||||
Future<StorageService> reinitialize(StorageService s) async {
|
||||
BaseSharedPreferencesService.resetForTesting();
|
||||
return StorageService.getInstance();
|
||||
}
|
||||
|
||||
test('moves full-profile-id-scoped keys onto the uuid scope', () async {
|
||||
var s = await StorageService.getInstance();
|
||||
await s.prefs.setString('user_${fullId}_selected_library_key', 'lib-1');
|
||||
await s.prefs.setBool('user_${fullId}_some_flag', true);
|
||||
await s.prefs.setStringList('user_${fullId}_hidden_libraries', ['a', 'b']);
|
||||
|
||||
s = await reinitialize(s);
|
||||
|
||||
expect(s.prefs.getString('user_${uuid}_selected_library_key'), 'lib-1');
|
||||
expect(s.prefs.getBool('user_${uuid}_some_flag'), isTrue);
|
||||
expect(s.prefs.getStringList('user_${uuid}_hidden_libraries'), ['a', 'b']);
|
||||
expect(s.prefs.keys.where((k) => k.contains('plex-home-')), isEmpty);
|
||||
});
|
||||
|
||||
test('full-id value wins over a stale pre-migration uuid-scoped value', () async {
|
||||
var s = await StorageService.getInstance();
|
||||
await s.prefs.setString('user_${uuid}_selected_library_key', 'stale');
|
||||
await s.prefs.setString('user_${fullId}_selected_library_key', 'fresh');
|
||||
|
||||
s = await reinitialize(s);
|
||||
|
||||
expect(s.prefs.getString('user_${uuid}_selected_library_key'), 'fresh');
|
||||
});
|
||||
|
||||
test('leaves local-profile scopes and unparseable plex-home scopes untouched', () async {
|
||||
var s = await StorageService.getInstance();
|
||||
await s.prefs.setString('user_local-1_selected_library_key', 'keep');
|
||||
await s.prefs.setString('user_plex-home-acct-not-a-uuid_key', 'keep-too');
|
||||
|
||||
s = await reinitialize(s);
|
||||
|
||||
expect(s.prefs.getString('user_local-1_selected_library_key'), 'keep');
|
||||
expect(s.prefs.getString('user_plex-home-acct-not-a-uuid_key'), 'keep-too');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// clearCredentials
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user