| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- import 'dart:typed_data';
- import 'package:cardash_launcher/data/models/jellyfin_library.dart';
- import 'package:cardash_launcher/data/models/jellyfin_models.dart';
- import 'package:cardash_launcher/data/services/jellyfin_library_service.dart';
- import 'package:flutter_test/flutter_test.dart';
- import 'jellyfin_fakes.dart';
- void main() {
- test(
- 'recently played dedupes tracks by album and takes the newest five',
- () async {
- final gateway = FakeJellyfinLibraryGateway()
- ..played = [
- track(
- 'a2',
- albumId: 'album-a',
- albumName: 'Album A',
- artist: 'Artist A',
- lastPlayed: DateTime.utc(2026, 10, 31, 12),
- ),
- track(
- 'a1',
- albumId: 'album-a',
- albumName: 'Album A',
- artist: 'Artist A',
- lastPlayed: DateTime.utc(2026, 10, 31, 11),
- ),
- track('b1', albumId: 'album-b', albumName: 'Album B', artist: 'B'),
- track('c1', albumId: 'album-c', albumName: 'Album C', artist: 'C'),
- track('d1', albumId: 'album-d', albumName: 'Album D', artist: 'D'),
- track('e1', albumId: 'album-e', albumName: 'Album E', artist: 'E'),
- track('f1', albumId: 'album-f', albumName: 'Album F', artist: 'F'),
- ];
- final service = JellyfinLibraryService(
- connectionProvider: () => testJellyfinConnection,
- gateway: gateway,
- );
- final albums = await service.latestPlayed();
- expect(albums.map((album) => album.id), [
- 'album-a',
- 'album-b',
- 'album-c',
- 'album-d',
- 'album-e',
- ]);
- expect(albums.first.name, 'Album A');
- expect(albums.first.lastPlayed, DateTime.utc(2026, 10, 31, 12));
- expect(gateway.playedCalls, 1);
- },
- );
- test('latest added delegates to the album query', () async {
- final gateway = FakeJellyfinLibraryGateway()
- ..added = [album('one'), album('two')];
- final service = JellyfinLibraryService(
- connectionProvider: () => testJellyfinConnection,
- gateway: gateway,
- );
- final albums = await service.latestAdded(limit: 2);
- expect(albums.map((entry) => entry.id), ['one', 'two']);
- expect(gateway.addedCalls, 1);
- });
- test('genre families are cached and can be forced', () async {
- final gateway = FakeJellyfinLibraryGateway()
- ..genres = const ['Metal', 'Jazz', 'Рок'];
- final service = JellyfinLibraryService(
- connectionProvider: () => testJellyfinConnection,
- gateway: gateway,
- );
- final first = await service.genreFamilies();
- expect(first.map((family) => family.label), ['Metal', 'Rock', 'Jazz']);
- expect(gateway.genreCalls, 1);
- await service.genreFamilies();
- expect(gateway.genreCalls, 1);
- await service.genreFamilies(force: true);
- expect(gateway.genreCalls, 2);
- });
- test('random albums expand only the selected families', () async {
- final gateway = FakeJellyfinLibraryGateway()
- ..genres = const ['Metal', 'Melodic Metal', 'Метъл', 'Jazz', 'Рок']
- ..random = [album('r1'), album('r2')];
- final service = JellyfinLibraryService(
- connectionProvider: () => testJellyfinConnection,
- gateway: gateway,
- );
- final albums = await service.randomAlbums(
- selections: const [
- JellyfinGenreSelection(id: 'metal', label: 'Metal'),
- JellyfinGenreSelection(id: 'jazz', label: 'Jazz'),
- ],
- );
- expect(albums.map((entry) => entry.id), ['r1', 'r2']);
- expect(
- gateway.lastRandomGenres,
- containsAll(['Metal', 'Melodic Metal', 'Метъл', 'Jazz']),
- );
- expect(gateway.lastRandomGenres, isNot(contains('Рок')));
- });
- test('no selected genres means no random request', () async {
- final gateway = FakeJellyfinLibraryGateway();
- final service = JellyfinLibraryService(
- connectionProvider: () => testJellyfinConnection,
- gateway: gateway,
- );
- final albums = await service.randomAlbums(selections: const []);
- expect(albums, isEmpty);
- expect(gateway.randomCalls, 0);
- });
- test('stream URL carries no token and uses the universal endpoint', () {
- final gateway = FakeJellyfinLibraryGateway();
- final service = JellyfinLibraryService(
- connectionProvider: () => testJellyfinConnection,
- gateway: gateway,
- );
- final url = service.streamUrl(
- track('a1', mediaSourceId: 'source-a1'),
- maxStreamingBitrate: 320000,
- );
- final text = url.toString();
- expect(text, contains('Audio/a1/universal'));
- expect(text, contains('MediaSourceId=source-a1'));
- expect(text, contains('MaxStreamingBitrate=320000'));
- expect(text, contains('EnableRedirection=true'));
- expect(text, isNot(contains('test-token')));
- expect(text, isNot(contains('api_key')));
- });
- test('buildQueue produces a native queue with artwork and auth', () async {
- final gateway = FakeJellyfinLibraryGateway()
- ..tracks = {
- 'album-a': [
- track('a1', duration: const Duration(seconds: 60)),
- track('a2', duration: const Duration(seconds: 120)),
- ],
- }
- ..artwork = Uint8List.fromList([1, 2, 3]);
- final service = JellyfinLibraryService(
- connectionProvider: () => testJellyfinConnection,
- gateway: gateway,
- );
- final queue = await service.buildQueue(
- album('album-a', imageTag: 'tag-a'),
- playSessionId: 'session-1',
- );
- expect(queue, isNotNull);
- expect(queue!.items.length, 2);
- expect(queue.items.first.trackId, 'a1');
- expect(queue.items.first.durationMs, 60000);
- expect(queue.items.first.url, contains('Audio/a1/universal'));
- expect(queue.playSessionId, 'session-1');
- expect(queue.authorization, contains('Token="test-token"'));
- expect(queue.artwork, [1, 2, 3]);
- expect(queue.items.first.url, isNot(contains('test-token')));
- });
- test(
- 'buildQueue returns null for an album with no playable tracks',
- () async {
- final gateway = FakeJellyfinLibraryGateway();
- final service = JellyfinLibraryService(
- connectionProvider: () => testJellyfinConnection,
- gateway: gateway,
- );
- final queue = await service.buildQueue(album('empty'));
- expect(queue, isNull);
- },
- );
- test(
- 'image URL and headers are available without exposing the token in the URL',
- () {
- final gateway = FakeJellyfinLibraryGateway();
- final service = JellyfinLibraryService(
- connectionProvider: () => testJellyfinConnection,
- gateway: gateway,
- );
- final url = service.imageUrl('album-a', 'tag-a', maxHeight: 300);
- expect(url, isNotNull);
- expect(url.toString(), contains('Items/album-a/Images/Primary'));
- expect(url.toString(), contains('maxHeight=300'));
- expect(url.toString(), contains('tag=tag-a'));
- expect(url.toString(), isNot(contains('test-token')));
- expect(
- service.imageHeaders['Authorization'],
- contains('Token="test-token"'),
- );
- },
- );
- test('a missing connection fails without touching the gateway', () async {
- final gateway = FakeJellyfinLibraryGateway();
- final service = JellyfinLibraryService(
- connectionProvider: () => null,
- gateway: gateway,
- );
- expect(service.isConfigured, isFalse);
- await expectLater(
- service.latestPlayed(),
- throwsA(isA<JellyfinException>()),
- );
- expect(gateway.playedCalls, 0);
- });
- }
|