jellyfin_playback.dart 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import 'package:flutter/foundation.dart';
  2. import '../../core/app_identity.dart';
  3. import 'media_playback.dart';
  4. /// One track in the native Jellyfin playback queue.
  5. @immutable
  6. class JellyfinQueueItem {
  7. const JellyfinQueueItem({
  8. required this.trackId,
  9. required this.mediaSourceId,
  10. required this.url,
  11. required this.title,
  12. required this.artist,
  13. required this.album,
  14. required this.durationMs,
  15. required this.index,
  16. });
  17. final String trackId;
  18. final String mediaSourceId;
  19. final String url;
  20. final String title;
  21. final String artist;
  22. final String album;
  23. final int durationMs;
  24. final int index;
  25. Map<String, Object?> toMap() => {
  26. 'trackId': trackId,
  27. 'mediaSourceId': mediaSourceId,
  28. 'url': url,
  29. 'title': title,
  30. 'artist': artist,
  31. 'album': album,
  32. 'durationMs': durationMs,
  33. 'index': index,
  34. };
  35. }
  36. /// Everything the native service needs to start one album. [authorization] and
  37. /// [accessToken] are kept in memory and are never logged or persisted.
  38. @immutable
  39. class JellyfinPlaybackQueue {
  40. const JellyfinPlaybackQueue({
  41. required this.baseUrl,
  42. required this.authorization,
  43. required this.accessToken,
  44. required this.userId,
  45. required this.deviceId,
  46. required this.playSessionId,
  47. required this.albumId,
  48. required this.albumName,
  49. required this.artistName,
  50. required this.items,
  51. this.artwork,
  52. });
  53. final Uri baseUrl;
  54. final String authorization;
  55. final String accessToken;
  56. final String userId;
  57. final String deviceId;
  58. final String playSessionId;
  59. final String albumId;
  60. final String albumName;
  61. final String artistName;
  62. final List<JellyfinQueueItem> items;
  63. final Uint8List? artwork;
  64. Map<String, Object?> toMap() => {
  65. 'baseUrl': baseUrl.toString(),
  66. 'authorization': authorization,
  67. 'accessToken': accessToken,
  68. 'userId': userId,
  69. 'deviceId': deviceId,
  70. 'playSessionId': playSessionId,
  71. 'albumId': albumId,
  72. 'albumName': albumName,
  73. 'artistName': artistName,
  74. 'artwork': artwork,
  75. 'items': items.map((item) => item.toMap()).toList(growable: false),
  76. };
  77. }
  78. /// The native player's current state. Positions are milliseconds.
  79. @immutable
  80. class JellyfinPlaybackSnapshot {
  81. const JellyfinPlaybackSnapshot({
  82. this.available = false,
  83. this.playing = false,
  84. this.buffering = false,
  85. this.itemId,
  86. this.albumId,
  87. this.title,
  88. this.artist,
  89. this.album,
  90. this.durationMs = 0,
  91. this.positionMs = 0,
  92. this.index = 0,
  93. this.hasPrevious = false,
  94. this.hasNext = false,
  95. this.error,
  96. });
  97. static const empty = JellyfinPlaybackSnapshot();
  98. final bool available;
  99. final bool playing;
  100. final bool buffering;
  101. final String? itemId;
  102. final String? albumId;
  103. final String? title;
  104. final String? artist;
  105. final String? album;
  106. final int durationMs;
  107. final int positionMs;
  108. final int index;
  109. final bool hasPrevious;
  110. final bool hasNext;
  111. final String? error;
  112. bool get playingOrBuffering => playing || buffering;
  113. bool get ended =>
  114. available && !playing && durationMs > 0 && positionMs >= durationMs - 500;
  115. double get progress {
  116. if (durationMs <= 0) return 0;
  117. return (positionMs / durationMs).clamp(0.0, 1.0);
  118. }
  119. JellyfinPlaybackSnapshot copyWith({
  120. bool? available,
  121. bool? playing,
  122. bool? buffering,
  123. int? positionMs,
  124. int? index,
  125. bool? hasPrevious,
  126. bool? hasNext,
  127. String? error,
  128. }) {
  129. return JellyfinPlaybackSnapshot(
  130. available: available ?? this.available,
  131. playing: playing ?? this.playing,
  132. buffering: buffering ?? this.buffering,
  133. itemId: itemId,
  134. albumId: albumId,
  135. title: title,
  136. artist: artist,
  137. album: album,
  138. durationMs: durationMs,
  139. positionMs: positionMs ?? this.positionMs,
  140. index: index ?? this.index,
  141. hasPrevious: hasPrevious ?? this.hasPrevious,
  142. hasNext: hasNext ?? this.hasNext,
  143. error: error ?? this.error,
  144. );
  145. }
  146. factory JellyfinPlaybackSnapshot.fromMap(Map<dynamic, dynamic> map) {
  147. int integer(String key) => (map[key] as num?)?.toInt() ?? 0;
  148. String? string(String key) {
  149. final value = (map[key] as String?)?.trim();
  150. return value == null || value.isEmpty ? null : value;
  151. }
  152. return JellyfinPlaybackSnapshot(
  153. available: map['available'] == true,
  154. playing: map['playing'] == true,
  155. buffering: map['buffering'] == true,
  156. itemId: string('itemId'),
  157. albumId: string('albumId'),
  158. title: string('title'),
  159. artist: string('artist'),
  160. album: string('album'),
  161. durationMs: integer('durationMs'),
  162. positionMs: integer('positionMs'),
  163. index: integer('index'),
  164. hasPrevious: map['hasPrevious'] == true,
  165. hasNext: map['hasNext'] == true,
  166. error: string('error'),
  167. );
  168. }
  169. /// Snapshot consumed by the Home volume card and the existing media widgets.
  170. MediaPlayback toMediaPlayback() {
  171. return MediaPlayback(
  172. hasAccess: available,
  173. hasSession: available && itemId != null,
  174. playing: playing,
  175. title: title,
  176. artist: artist,
  177. packageName: appPackageName,
  178. durationMs: durationMs,
  179. positionMs: positionMs,
  180. );
  181. }
  182. @override
  183. bool operator ==(Object other) {
  184. return other is JellyfinPlaybackSnapshot &&
  185. other.available == available &&
  186. other.playing == playing &&
  187. other.buffering == buffering &&
  188. other.itemId == itemId &&
  189. other.albumId == albumId &&
  190. other.title == title &&
  191. other.artist == artist &&
  192. other.album == album &&
  193. other.durationMs == durationMs &&
  194. other.positionMs == positionMs &&
  195. other.index == index &&
  196. other.hasPrevious == hasPrevious &&
  197. other.hasNext == hasNext &&
  198. other.error == error;
  199. }
  200. @override
  201. int get hashCode => Object.hash(
  202. available,
  203. playing,
  204. buffering,
  205. itemId,
  206. albumId,
  207. title,
  208. artist,
  209. album,
  210. durationMs,
  211. positionMs,
  212. index,
  213. hasPrevious,
  214. hasNext,
  215. error,
  216. );
  217. }