jellyfin_fakes.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import 'dart:typed_data';
  2. import 'package:cardash_launcher/data/models/app_entry.dart';
  3. import 'package:cardash_launcher/data/models/jellyfin_library.dart';
  4. import 'package:cardash_launcher/data/models/jellyfin_models.dart';
  5. import 'package:cardash_launcher/data/models/jellyfin_playback.dart';
  6. import 'package:cardash_launcher/data/models/media_playback.dart';
  7. import 'package:cardash_launcher/data/models/system_status.dart';
  8. import 'package:cardash_launcher/data/models/update_info.dart';
  9. import 'package:cardash_launcher/data/repositories/settings_repository.dart';
  10. import 'package:cardash_launcher/data/services/jellyfin_library_service.dart';
  11. import 'package:cardash_launcher/data/services/jellyfin_playback_platform.dart';
  12. import 'package:cardash_launcher/data/services/jellyfin_service.dart';
  13. import 'package:cardash_launcher/data/services/native_bridge.dart';
  14. import 'package:cardash_launcher/data/services/secure_store.dart';
  15. import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
  16. import 'package:shared_preferences/shared_preferences.dart';
  17. final testJellyfinConnection = JellyfinConnection(
  18. baseUrl: Uri.parse('https://jellyfin.example.com/'),
  19. accessToken: 'test-token',
  20. userId: 'user-1',
  21. deviceId: 'device-1',
  22. userName: 'wasp',
  23. );
  24. JellyfinAlbum album(String id, {String? imageTag}) {
  25. return JellyfinAlbum(
  26. id: id,
  27. name: 'Album $id',
  28. artist: 'Artist $id',
  29. imageTag: imageTag,
  30. );
  31. }
  32. JellyfinTrack track(
  33. String id, {
  34. String albumId = 'album-1',
  35. String albumName = 'Album 1',
  36. String artist = 'Artist 1',
  37. int index = 1,
  38. Duration duration = const Duration(minutes: 3),
  39. String? mediaSourceId,
  40. DateTime? lastPlayed,
  41. }) {
  42. return JellyfinTrack(
  43. id: id,
  44. name: 'Track $id',
  45. albumId: albumId,
  46. album: albumName,
  47. artist: artist,
  48. indexNumber: index,
  49. parentIndexNumber: 1,
  50. duration: duration,
  51. mediaSourceId: mediaSourceId ?? 'source-$id',
  52. container: 'mp3',
  53. codec: 'mp3',
  54. lastPlayed: lastPlayed,
  55. );
  56. }
  57. class MemorySecureStore implements SecureStore {
  58. final values = <String, String>{};
  59. @override
  60. Future<String?> read(String key) async => values[key];
  61. @override
  62. Future<void> write(String key, String value) async {
  63. values[key] = value;
  64. }
  65. @override
  66. Future<void> delete(String key) async {
  67. values.remove(key);
  68. }
  69. }
  70. class FakeJellyfinGateway implements JellyfinGateway {
  71. @override
  72. Future<JellyfinServerInfo> serverInfo(Uri baseUrl) async {
  73. return const JellyfinServerInfo(name: 'Test server', version: '12.2.0');
  74. }
  75. @override
  76. Future<JellyfinQuickConnect> startQuickConnect(
  77. Uri baseUrl, {
  78. required String deviceId,
  79. }) async {
  80. return const JellyfinQuickConnect(code: '123456', secret: 'secret');
  81. }
  82. @override
  83. Future<bool> isQuickConnectApproved(Uri baseUrl, String secret) async {
  84. return false;
  85. }
  86. @override
  87. Future<JellyfinSession> authenticateQuickConnect(
  88. Uri baseUrl,
  89. String secret, {
  90. required String deviceId,
  91. }) async {
  92. return const JellyfinSession(
  93. accessToken: 'test-token',
  94. userId: 'user-1',
  95. userName: 'wasp',
  96. );
  97. }
  98. @override
  99. Future<JellyfinUser> currentUser(Uri baseUrl, String token) async {
  100. return const JellyfinUser(id: 'user-1', name: 'wasp');
  101. }
  102. @override
  103. void close() {}
  104. }
  105. class FakeSystemPlatform implements LauncherPlatform {
  106. @override
  107. Future<List<AppEntry>> listApps() async => const [];
  108. @override
  109. Future<void> launchApp({
  110. required String packageName,
  111. required String activityName,
  112. }) async {}
  113. @override
  114. Future<bool> isDefaultHome() async => true;
  115. @override
  116. Future<bool> requestDefaultHome() async => true;
  117. @override
  118. Future<SystemStatus> getStatus() async =>
  119. const SystemStatus(batteryLevel: 80, charging: false, wifi: true);
  120. @override
  121. Future<VolumeInfo> getVolume() async => const VolumeInfo(index: 5, max: 15);
  122. @override
  123. Future<void> setVolume(int index) async {}
  124. @override
  125. Future<bool> isMusicPlaying() async => false;
  126. @override
  127. Future<bool> sendMediaKey(String action) async => true;
  128. @override
  129. Future<MediaPlayback> getMediaState() async => const MediaPlayback();
  130. @override
  131. Future<bool> openNotificationAccessSettings() async => true;
  132. @override
  133. Future<void> setKeepScreenOn(bool enabled) async {}
  134. @override
  135. Future<bool> openUrl(String url) async => true;
  136. @override
  137. Future<UpdateInstallResult> downloadAndInstallUpdate(String url) async {
  138. return const UpdateInstallResult(started: false, permissionRequired: false);
  139. }
  140. }
  141. class FakeJellyfinLibraryGateway implements JellyfinLibraryGateway {
  142. List<JellyfinTrack> played = const [];
  143. List<JellyfinAlbum> added = const [];
  144. List<JellyfinAlbum> random = const [];
  145. Map<String, List<JellyfinTrack>> tracks = const {};
  146. List<String> libraryIds = const ['library-1'];
  147. List<String> genres = const [];
  148. Uint8List? artwork;
  149. Object? error;
  150. Object? randomError;
  151. int playedCalls = 0;
  152. int addedCalls = 0;
  153. int randomCalls = 0;
  154. int genreCalls = 0;
  155. int trackCalls = 0;
  156. int artworkCalls = 0;
  157. List<String>? lastRandomGenres;
  158. String? lastAlbumId;
  159. void resetCounters() {
  160. playedCalls = 0;
  161. addedCalls = 0;
  162. randomCalls = 0;
  163. genreCalls = 0;
  164. trackCalls = 0;
  165. artworkCalls = 0;
  166. }
  167. @override
  168. Future<List<JellyfinTrack>> playedTracks(
  169. JellyfinConnection connection, {
  170. int limit = 50,
  171. }) async {
  172. playedCalls++;
  173. if (error != null) throw error!;
  174. return played.take(limit).toList(growable: false);
  175. }
  176. @override
  177. Future<List<JellyfinAlbum>> latestAddedAlbums(
  178. JellyfinConnection connection, {
  179. int limit = 5,
  180. }) async {
  181. addedCalls++;
  182. if (error != null) throw error!;
  183. return added.take(limit).toList(growable: false);
  184. }
  185. @override
  186. Future<List<JellyfinAlbum>> randomAlbums(
  187. JellyfinConnection connection, {
  188. required List<String> genres,
  189. int limit = 5,
  190. }) async {
  191. randomCalls++;
  192. lastRandomGenres = List<String>.from(genres);
  193. if (randomError != null) throw randomError!;
  194. return random.take(limit).toList(growable: false);
  195. }
  196. @override
  197. Future<List<JellyfinTrack>> albumTracks(
  198. JellyfinConnection connection,
  199. String albumId,
  200. ) async {
  201. trackCalls++;
  202. lastAlbumId = albumId;
  203. if (error != null) throw error!;
  204. return tracks[albumId] ?? const [];
  205. }
  206. @override
  207. Future<List<String>> musicLibraryIds(JellyfinConnection connection) async {
  208. return libraryIds;
  209. }
  210. @override
  211. Future<List<String>> genreNames(
  212. JellyfinConnection connection,
  213. String libraryId,
  214. ) async {
  215. genreCalls++;
  216. if (error != null) throw error!;
  217. return genres;
  218. }
  219. @override
  220. Future<Uint8List?> albumArtwork(
  221. JellyfinConnection connection,
  222. String albumId, {
  223. String? tag,
  224. int maxHeight = 400,
  225. }) async {
  226. artworkCalls++;
  227. return artwork;
  228. }
  229. }
  230. Future<JellyfinController> configuredJellyfinController({
  231. FakeJellyfinGateway? gateway,
  232. MemorySecureStore? store,
  233. }) async {
  234. SharedPreferences.setMockInitialValues({
  235. 'jellyfin.enabled': true,
  236. 'jellyfin.serverUrl': 'https://jellyfin.example.com',
  237. 'jellyfin.userId': 'user-1',
  238. 'jellyfin.userName': 'wasp',
  239. 'jellyfin.serverName': 'Test server',
  240. });
  241. final prefs = await SharedPreferences.getInstance();
  242. final settings = SettingsRepository(prefs);
  243. final secure = store ?? MemorySecureStore();
  244. secure.values[JellyfinController.tokenKey] = 'test-token';
  245. final controller = JellyfinController(
  246. settings,
  247. gateway ?? FakeJellyfinGateway(),
  248. secure,
  249. );
  250. await controller.initialize();
  251. return controller;
  252. }
  253. JellyfinLibraryService testLibraryService(
  254. JellyfinController controller,
  255. FakeJellyfinLibraryGateway gateway,
  256. ) {
  257. return JellyfinLibraryService(
  258. connectionProvider: () => controller.connection ?? testJellyfinConnection,
  259. gateway: gateway,
  260. );
  261. }
  262. class FakeJellyfinPlaybackPlatform implements JellyfinPlaybackPlatform {
  263. JellyfinPlaybackQueue? queue;
  264. JellyfinPlaybackSnapshot state = JellyfinPlaybackSnapshot.empty;
  265. final List<String> commands = [];
  266. int setQueueCalls = 0;
  267. bool setQueueResult = true;
  268. bool permissionGranted = false;
  269. @override
  270. Future<bool> setQueue(JellyfinPlaybackQueue queue) async {
  271. commands.add('setQueue');
  272. setQueueCalls++;
  273. if (!setQueueResult) return false;
  274. this.queue = queue;
  275. state = snapshotAt(0, playing: true);
  276. return true;
  277. }
  278. JellyfinPlaybackSnapshot snapshotAt(
  279. int index, {
  280. bool playing = false,
  281. int positionMs = 0,
  282. }) {
  283. final current = queue;
  284. if (current == null || current.items.isEmpty) {
  285. return JellyfinPlaybackSnapshot.empty;
  286. }
  287. final safeIndex = index.clamp(0, current.items.length - 1);
  288. final item = current.items[safeIndex];
  289. return JellyfinPlaybackSnapshot(
  290. available: true,
  291. playing: playing,
  292. itemId: item.trackId,
  293. albumId: current.albumId,
  294. title: item.title,
  295. artist: item.artist,
  296. album: item.album,
  297. durationMs: item.durationMs,
  298. positionMs: positionMs,
  299. index: safeIndex,
  300. hasPrevious: safeIndex > 0,
  301. hasNext: safeIndex < current.items.length - 1,
  302. );
  303. }
  304. @override
  305. Future<bool> play() async {
  306. commands.add('play');
  307. state = state.copyWith(playing: true);
  308. return true;
  309. }
  310. @override
  311. Future<bool> pause() async {
  312. commands.add('pause');
  313. state = state.copyWith(playing: false);
  314. return true;
  315. }
  316. @override
  317. Future<bool> next() async {
  318. commands.add('next');
  319. final current = queue;
  320. if (current == null || !state.hasNext) return false;
  321. state = snapshotAt(state.index + 1, playing: state.playing, positionMs: 0);
  322. return true;
  323. }
  324. @override
  325. Future<bool> previous() async {
  326. commands.add('previous');
  327. if (!state.hasPrevious) return false;
  328. state = snapshotAt(state.index - 1, playing: state.playing, positionMs: 0);
  329. return true;
  330. }
  331. @override
  332. Future<bool> seekToIndex(int index) async {
  333. commands.add('seekToIndex:$index');
  334. final current = queue;
  335. if (current == null || index < 0 || index >= current.items.length) {
  336. return false;
  337. }
  338. state = snapshotAt(index, playing: state.playing, positionMs: 0);
  339. return true;
  340. }
  341. @override
  342. Future<bool> seek(Duration position) async {
  343. commands.add('seek:${position.inMilliseconds}');
  344. state = state.copyWith(positionMs: position.inMilliseconds);
  345. return true;
  346. }
  347. @override
  348. Future<bool> stop() async {
  349. commands.add('stop');
  350. state = JellyfinPlaybackSnapshot.empty;
  351. queue = null;
  352. return true;
  353. }
  354. @override
  355. Future<JellyfinPlaybackSnapshot> getState() async {
  356. commands.add('getState');
  357. return state;
  358. }
  359. @override
  360. Future<bool> notificationPermissionGranted() async => permissionGranted;
  361. @override
  362. Future<bool> requestNotificationPermission() async {
  363. permissionGranted = true;
  364. return true;
  365. }
  366. @override
  367. void dispose() {}
  368. }