jellyfin_library_service_test.dart 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import 'dart:typed_data';
  2. import 'package:cardash_launcher/data/models/jellyfin_library.dart';
  3. import 'package:cardash_launcher/data/models/jellyfin_models.dart';
  4. import 'package:cardash_launcher/data/services/jellyfin_library_service.dart';
  5. import 'package:flutter_test/flutter_test.dart';
  6. import 'jellyfin_fakes.dart';
  7. void main() {
  8. test(
  9. 'recently played dedupes tracks by album and takes the newest five',
  10. () async {
  11. final gateway = FakeJellyfinLibraryGateway()
  12. ..played = [
  13. track(
  14. 'a2',
  15. albumId: 'album-a',
  16. albumName: 'Album A',
  17. artist: 'Artist A',
  18. lastPlayed: DateTime.utc(2026, 10, 31, 12),
  19. ),
  20. track(
  21. 'a1',
  22. albumId: 'album-a',
  23. albumName: 'Album A',
  24. artist: 'Artist A',
  25. lastPlayed: DateTime.utc(2026, 10, 31, 11),
  26. ),
  27. track('b1', albumId: 'album-b', albumName: 'Album B', artist: 'B'),
  28. track('c1', albumId: 'album-c', albumName: 'Album C', artist: 'C'),
  29. track('d1', albumId: 'album-d', albumName: 'Album D', artist: 'D'),
  30. track('e1', albumId: 'album-e', albumName: 'Album E', artist: 'E'),
  31. track('f1', albumId: 'album-f', albumName: 'Album F', artist: 'F'),
  32. ];
  33. final service = JellyfinLibraryService(
  34. connectionProvider: () => testJellyfinConnection,
  35. gateway: gateway,
  36. );
  37. final albums = await service.latestPlayed();
  38. expect(albums.map((album) => album.id), [
  39. 'album-a',
  40. 'album-b',
  41. 'album-c',
  42. 'album-d',
  43. 'album-e',
  44. ]);
  45. expect(albums.first.name, 'Album A');
  46. expect(albums.first.lastPlayed, DateTime.utc(2026, 10, 31, 12));
  47. expect(gateway.playedCalls, 1);
  48. },
  49. );
  50. test('latest added delegates to the album query', () async {
  51. final gateway = FakeJellyfinLibraryGateway()
  52. ..added = [album('one'), album('two')];
  53. final service = JellyfinLibraryService(
  54. connectionProvider: () => testJellyfinConnection,
  55. gateway: gateway,
  56. );
  57. final albums = await service.latestAdded(limit: 2);
  58. expect(albums.map((entry) => entry.id), ['one', 'two']);
  59. expect(gateway.addedCalls, 1);
  60. });
  61. test('genre families are cached and can be forced', () async {
  62. final gateway = FakeJellyfinLibraryGateway()
  63. ..genres = const ['Metal', 'Jazz', 'Рок'];
  64. final service = JellyfinLibraryService(
  65. connectionProvider: () => testJellyfinConnection,
  66. gateway: gateway,
  67. );
  68. final first = await service.genreFamilies();
  69. expect(first.map((family) => family.label), ['Metal', 'Rock', 'Jazz']);
  70. expect(gateway.genreCalls, 1);
  71. await service.genreFamilies();
  72. expect(gateway.genreCalls, 1);
  73. await service.genreFamilies(force: true);
  74. expect(gateway.genreCalls, 2);
  75. });
  76. test('random albums expand only the selected families', () async {
  77. final gateway = FakeJellyfinLibraryGateway()
  78. ..genres = const ['Metal', 'Melodic Metal', 'Метъл', 'Jazz', 'Рок']
  79. ..random = [album('r1'), album('r2')];
  80. final service = JellyfinLibraryService(
  81. connectionProvider: () => testJellyfinConnection,
  82. gateway: gateway,
  83. );
  84. final albums = await service.randomAlbums(
  85. selections: const [
  86. JellyfinGenreSelection(id: 'metal', label: 'Metal'),
  87. JellyfinGenreSelection(id: 'jazz', label: 'Jazz'),
  88. ],
  89. );
  90. expect(albums.map((entry) => entry.id), ['r1', 'r2']);
  91. expect(
  92. gateway.lastRandomGenres,
  93. containsAll(['Metal', 'Melodic Metal', 'Метъл', 'Jazz']),
  94. );
  95. expect(gateway.lastRandomGenres, isNot(contains('Рок')));
  96. });
  97. test('no selected genres means no random request', () async {
  98. final gateway = FakeJellyfinLibraryGateway();
  99. final service = JellyfinLibraryService(
  100. connectionProvider: () => testJellyfinConnection,
  101. gateway: gateway,
  102. );
  103. final albums = await service.randomAlbums(selections: const []);
  104. expect(albums, isEmpty);
  105. expect(gateway.randomCalls, 0);
  106. });
  107. test('stream URL carries no token and uses the universal endpoint', () {
  108. final gateway = FakeJellyfinLibraryGateway();
  109. final service = JellyfinLibraryService(
  110. connectionProvider: () => testJellyfinConnection,
  111. gateway: gateway,
  112. );
  113. final url = service.streamUrl(
  114. track('a1', mediaSourceId: 'source-a1'),
  115. maxStreamingBitrate: 320000,
  116. );
  117. final text = url.toString();
  118. expect(text, contains('Audio/a1/universal'));
  119. expect(text, contains('MediaSourceId=source-a1'));
  120. expect(text, contains('MaxStreamingBitrate=320000'));
  121. expect(text, contains('EnableRedirection=true'));
  122. expect(text, isNot(contains('test-token')));
  123. expect(text, isNot(contains('api_key')));
  124. });
  125. test('buildQueue produces a native queue with artwork and auth', () async {
  126. final gateway = FakeJellyfinLibraryGateway()
  127. ..tracks = {
  128. 'album-a': [
  129. track('a1', duration: const Duration(seconds: 60)),
  130. track('a2', duration: const Duration(seconds: 120)),
  131. ],
  132. }
  133. ..artwork = Uint8List.fromList([1, 2, 3]);
  134. final service = JellyfinLibraryService(
  135. connectionProvider: () => testJellyfinConnection,
  136. gateway: gateway,
  137. );
  138. final queue = await service.buildQueue(
  139. album('album-a', imageTag: 'tag-a'),
  140. playSessionId: 'session-1',
  141. );
  142. expect(queue, isNotNull);
  143. expect(queue!.items.length, 2);
  144. expect(queue.items.first.trackId, 'a1');
  145. expect(queue.items.first.durationMs, 60000);
  146. expect(queue.items.first.url, contains('Audio/a1/universal'));
  147. expect(queue.playSessionId, 'session-1');
  148. expect(queue.authorization, contains('Token="test-token"'));
  149. expect(queue.artwork, [1, 2, 3]);
  150. expect(queue.items.first.url, isNot(contains('test-token')));
  151. });
  152. test(
  153. 'buildQueue returns null for an album with no playable tracks',
  154. () async {
  155. final gateway = FakeJellyfinLibraryGateway();
  156. final service = JellyfinLibraryService(
  157. connectionProvider: () => testJellyfinConnection,
  158. gateway: gateway,
  159. );
  160. final queue = await service.buildQueue(album('empty'));
  161. expect(queue, isNull);
  162. },
  163. );
  164. test(
  165. 'image URL and headers are available without exposing the token in the URL',
  166. () {
  167. final gateway = FakeJellyfinLibraryGateway();
  168. final service = JellyfinLibraryService(
  169. connectionProvider: () => testJellyfinConnection,
  170. gateway: gateway,
  171. );
  172. final url = service.imageUrl('album-a', 'tag-a', maxHeight: 300);
  173. expect(url, isNotNull);
  174. expect(url.toString(), contains('Items/album-a/Images/Primary'));
  175. expect(url.toString(), contains('maxHeight=300'));
  176. expect(url.toString(), contains('tag=tag-a'));
  177. expect(url.toString(), isNot(contains('test-token')));
  178. expect(
  179. service.imageHeaders['Authorization'],
  180. contains('Token="test-token"'),
  181. );
  182. },
  183. );
  184. test('a missing connection fails without touching the gateway', () async {
  185. final gateway = FakeJellyfinLibraryGateway();
  186. final service = JellyfinLibraryService(
  187. connectionProvider: () => null,
  188. gateway: gateway,
  189. );
  190. expect(service.isConfigured, isFalse);
  191. await expectLater(
  192. service.latestPlayed(),
  193. throwsA(isA<JellyfinException>()),
  194. );
  195. expect(gateway.playedCalls, 0);
  196. });
  197. }