feat(profiles): show the first linked connection's user picture
A local profile had no picture of its own and always fell back to
initials. It now borrows the user picture of the connection it was
linked to first — oldest Connection.createdAt, ties broken by
connection id, since the join table carries no creation time.
Jellyfin links resolve to /Users/{id}/Images/Primary, keyed by the
PrimaryImageTag now captured at authentication and refreshed from the
/Users/Me body checkHealth already fetches. That endpoint is anonymous
on every Jellyfin release, so the URL carries no api_key and the access
token stays out of the image cache key. Plex links resolve the Home
user the link points at against PlexHomeService's live cache, so no
account-level lookup is needed and the picture tracks Plex's own
refresh.
The picture is derived per snapshot and never written back onto a
Profile: ProfileDetailScreen upserts the model it holds, so a
persisted URL would go stale and outlive the connection it came from.
Plex Home profiles are untouched, including one whose Plex avatar is
unset — it keeps its initials rather than borrowing a lent connection's
picture.
close #1667
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/media_server_client.dart';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:fake_async/fake_async.dart';
|
||||
@@ -1016,6 +1017,150 @@ void main() {
|
||||
expect(persisted.single.isAdministrator, isTrue);
|
||||
});
|
||||
|
||||
test('persists a changed profile picture tag discovered during health checks', () async {
|
||||
final persisted = <JellyfinConnection>[];
|
||||
var requestCount = 0;
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _jellyfinConnection('user-a').copyWith(primaryImageTag: 'cached-tag'),
|
||||
httpClient: MockClient((request) async {
|
||||
requestCount++;
|
||||
expect(request.url.path, '/Users/Me');
|
||||
return http.Response(
|
||||
'{"Policy":{"IsAdministrator":false},"PrimaryImageTag":"fresh-tag"}',
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
final m = MultiServerManager()..onJellyfinConnectionUpdated = persisted.add;
|
||||
addTearDown(m.dispose);
|
||||
m.debugRegisterJellyfinClientForTesting(client);
|
||||
|
||||
final status = await client.checkHealth();
|
||||
|
||||
expect(status, HealthStatus.online);
|
||||
expect(requestCount, 1);
|
||||
expect(persisted, hasLength(1));
|
||||
expect(persisted.single.primaryImageTag, 'fresh-tag');
|
||||
});
|
||||
|
||||
test('clears the cached profile picture tag when the user deletes their avatar', () async {
|
||||
final persisted = <JellyfinConnection>[];
|
||||
var requestCount = 0;
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _jellyfinConnection('user-a').copyWith(primaryImageTag: 'cached-tag'),
|
||||
httpClient: MockClient((request) async {
|
||||
requestCount++;
|
||||
expect(request.url.path, '/Users/Me');
|
||||
return http.Response(
|
||||
'{"Policy":{"IsAdministrator":false}}',
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
final m = MultiServerManager()..onJellyfinConnectionUpdated = persisted.add;
|
||||
addTearDown(m.dispose);
|
||||
m.debugRegisterJellyfinClientForTesting(client);
|
||||
|
||||
final status = await client.checkHealth();
|
||||
|
||||
expect(status, HealthStatus.online);
|
||||
expect(requestCount, 1);
|
||||
expect(persisted, hasLength(1));
|
||||
expect(persisted.single.primaryImageTag, isNull);
|
||||
});
|
||||
|
||||
test('does not persist when the admin flag and profile picture tag are unchanged', () async {
|
||||
final persisted = <JellyfinConnection>[];
|
||||
var requestCount = 0;
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _jellyfinConnection('user-a').copyWith(primaryImageTag: 'same-tag'),
|
||||
httpClient: MockClient((request) async {
|
||||
requestCount++;
|
||||
expect(request.url.path, '/Users/Me');
|
||||
return http.Response(
|
||||
'{"Policy":{"IsAdministrator":false},"PrimaryImageTag":"same-tag"}',
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
final m = MultiServerManager()..onJellyfinConnectionUpdated = persisted.add;
|
||||
addTearDown(m.dispose);
|
||||
m.debugRegisterJellyfinClientForTesting(client);
|
||||
|
||||
final status = await client.checkHealth();
|
||||
|
||||
expect(status, HealthStatus.online);
|
||||
expect(requestCount, 1);
|
||||
expect(persisted, isEmpty);
|
||||
});
|
||||
|
||||
test('persists one connection update when the admin flag and profile picture tag both change', () async {
|
||||
final persisted = <JellyfinConnection>[];
|
||||
var requestCount = 0;
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _jellyfinConnection('user-a').copyWith(primaryImageTag: 'cached-tag'),
|
||||
httpClient: MockClient((request) async {
|
||||
requestCount++;
|
||||
expect(request.url.path, '/Users/Me');
|
||||
return http.Response(
|
||||
'{"Policy":{"IsAdministrator":true},"PrimaryImageTag":"fresh-tag"}',
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
final m = MultiServerManager()..onJellyfinConnectionUpdated = persisted.add;
|
||||
addTearDown(m.dispose);
|
||||
m.debugRegisterJellyfinClientForTesting(client);
|
||||
|
||||
final status = await client.checkHealth();
|
||||
|
||||
expect(status, HealthStatus.online);
|
||||
expect(requestCount, 1);
|
||||
expect(persisted, hasLength(1));
|
||||
expect(persisted.single.isAdministrator, isTrue);
|
||||
expect(persisted.single.primaryImageTag, 'fresh-tag');
|
||||
});
|
||||
|
||||
test('refreshes the profile picture tag when Policy is missing or malformed', () async {
|
||||
final responses = <Map<String, Object?>>[
|
||||
{'PrimaryImageTag': 'fresh-tag'},
|
||||
{'Policy': 'not-a-map', 'PrimaryImageTag': 'fresh-tag'},
|
||||
];
|
||||
|
||||
for (final responseBody in responses) {
|
||||
final persisted = <JellyfinConnection>[];
|
||||
var requestCount = 0;
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _jellyfinConnection('user-a').copyWith(primaryImageTag: 'cached-tag'),
|
||||
httpClient: MockClient((request) async {
|
||||
requestCount++;
|
||||
expect(request.url.path, '/Users/Me');
|
||||
return http.Response(jsonEncode(responseBody), 200, headers: {'content-type': 'application/json'});
|
||||
}),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
final m = MultiServerManager()..onJellyfinConnectionUpdated = persisted.add;
|
||||
addTearDown(m.dispose);
|
||||
m.debugRegisterJellyfinClientForTesting(client);
|
||||
|
||||
final status = await client.checkHealth();
|
||||
|
||||
expect(status, HealthStatus.online, reason: 'response: $responseBody');
|
||||
expect(requestCount, 1, reason: 'response: $responseBody');
|
||||
expect(persisted, hasLength(1), reason: 'response: $responseBody');
|
||||
expect(persisted.single.primaryImageTag, 'fresh-tag', reason: 'response: $responseBody');
|
||||
expect(persisted.single.isAdministrator, isFalse, reason: 'response: $responseBody');
|
||||
}
|
||||
});
|
||||
|
||||
test('health remains online when persisting refreshed admin status fails', () async {
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _jellyfinConnection('user-a'),
|
||||
|
||||
Reference in New Issue
Block a user