feat(jellyfin): favorites library filter + heart favorite in the rate sheet

close #1485
This commit is contained in:
edde746
2026-07-04 18:33:58 +02:00
parent 30de8be2cd
commit 7632d4bea7
52 changed files with 328 additions and 415 deletions
+12 -1
View File
@@ -1598,11 +1598,22 @@ void main() {
expect(captured!.path, '/Items/Filters');
expect(captured!.queryParameters['ParentId'], 'lib-1');
expect(captured!.queryParameters['userId'], 'user-1');
expect(result.filters.map((filter) => filter.filter), ['unwatched', 'genre', 'year', 'contentRating', 'tag']);
expect(result.filters.map((filter) => filter.filter), [
'unwatched',
'favorite',
'genre',
'year',
'contentRating',
'tag',
]);
expect(result.filters.first.filterType, 'boolean');
expect(result.filters.first.key, 'jellyfin:unwatched');
expect(result.filters.first.title, 'Unwatched');
expect(result.filters[1].filterType, 'boolean');
expect(result.filters[1].key, 'jellyfin:favorite');
expect(result.filters[1].title, 'Favorites');
expect(result.cachedValues.containsKey('unwatched'), isFalse);
expect(result.cachedValues.containsKey('favorite'), isFalse);
expect(result.cachedValues['genre']!.map((value) => value.key), ['Action', 'Drama']);
expect(result.cachedValues['year']!.map((value) => value.key), ['2024', '1999']);
});
+26
View File
@@ -100,6 +100,32 @@ void main() {
expect(item.isWatched, isFalse);
});
test('maps UserData.IsFavorite and ignores the legacy Likes flag', () {
final json = {
'Id': 'fav-1',
'Name': 'Favorited Movie',
'Type': 'Movie',
'UserData': {'IsFavorite': true, 'Likes': true},
};
final item = JellyfinMappers.mediaItem(json, serverId: ServerId(_serverId), absolutizer: null)!;
expect(item.isFavorite, isTrue);
// Likes used to map to userRating 10.0/0.0; the rate sheet now uses
// IsFavorite for Jellyfin, so Likes must no longer surface anywhere.
expect(item.userRating, isNull);
});
test('missing UserData leaves isFavorite null', () {
final item = JellyfinMappers.mediaItem(
{'Id': 'no-userdata', 'Name': 'No UserData', 'Type': 'Movie'},
serverId: ServerId(_serverId),
absolutizer: null,
)!;
expect(item.isFavorite, isNull);
});
test('maps generic Jellyfin video types to playable clips', () {
final video = JellyfinMappers.mediaItem(
{'Id': 'home-video', 'Name': 'Home Video', 'Type': 'Video'},
@@ -57,6 +57,10 @@ void main() {
expect(params['unwatched'], '1');
});
test('favoritesOnly is ignored (Plex has no favorites)', () {
expect(translator.toQueryParameters(const LibraryQuery(favoritesOnly: true)), isEmpty);
});
test('arbitrary filter clauses pass through verbatim', () {
final params = translator.toQueryParameters(
const LibraryQuery(
@@ -210,6 +214,16 @@ void main() {
expect(params['Filters'], 'IsUnplayed');
});
test('favoritesOnly sets Filters=IsFavorite', () {
final params = translator.toQueryParameters(const LibraryQuery(favoritesOnly: true));
expect(params['Filters'], 'IsFavorite');
});
test('unwatched + favorites combine into a comma-separated Filters list', () {
final params = translator.toQueryParameters(const LibraryQuery(includeWatched: false, favoritesOnly: true));
expect(params['Filters'], 'IsUnplayed,IsFavorite');
});
test('search puts text in SearchTerm', () {
final params = translator.toQueryParameters(const LibraryQuery(search: 'matrix'));
expect(params['SearchTerm'], 'matrix');
@@ -298,6 +312,14 @@ void main() {
expect(roundTrip(input)['director'], '12345');
});
test('favorite=1 maps to favoritesOnly, not a generic filter entry', () {
// Jellyfin-only key; the Plex translator deliberately drops it, so it
// must not leak into the verbatim-pass-through filters bucket either.
final query = libraryQueryFromPlexMap(map: {'favorite': '1'});
expect(query.favoritesOnly, isTrue);
expect(query.filters, isEmpty);
});
test('libraryKind argument overrides any type entry in the map', () {
// The browse tab always passes the library's actual kind; map's `type`
// is dropped if the explicit arg is present.