jellyfin_library.dart 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import 'package:flutter/foundation.dart';
  2. /// A saved Jellyfin selection in the genre picker.
  3. @immutable
  4. class JellyfinGenreSelection {
  5. const JellyfinGenreSelection({required this.id, required this.label});
  6. final String id;
  7. final String label;
  8. factory JellyfinGenreSelection.fromJson(Map<String, dynamic> json) {
  9. return JellyfinGenreSelection(
  10. id: (json['id'] as String?) ?? '',
  11. label: (json['label'] as String?) ?? '',
  12. );
  13. }
  14. Map<String, Object?> toJson() => {'id': id, 'label': label};
  15. @override
  16. bool operator ==(Object other) {
  17. return other is JellyfinGenreSelection &&
  18. other.id == id &&
  19. other.label == label;
  20. }
  21. @override
  22. int get hashCode => Object.hash(id, label);
  23. @override
  24. String toString() => label;
  25. }
  26. /// A canonical genre family such as Metal, Rock or Jazz. Raw genres keep their
  27. /// server spelling; [rawGenres] is what the random query expands to.
  28. @immutable
  29. class JellyfinGenreFamily {
  30. const JellyfinGenreFamily({
  31. required this.id,
  32. required this.label,
  33. required this.rawGenres,
  34. required this.isStandalone,
  35. });
  36. final String id;
  37. final String label;
  38. final List<String> rawGenres;
  39. final bool isStandalone;
  40. int get rawCount => rawGenres.length;
  41. @override
  42. bool operator ==(Object other) {
  43. return other is JellyfinGenreFamily && other.id == id;
  44. }
  45. @override
  46. int get hashCode => id.hashCode;
  47. }
  48. /// One raw genre on the server and the family it belongs to.
  49. @immutable
  50. class JellyfinGenre {
  51. const JellyfinGenre({
  52. required this.name,
  53. required this.familyId,
  54. required this.familyLabel,
  55. });
  56. final String name;
  57. final String familyId;
  58. final String familyLabel;
  59. }
  60. /// An album row in the Jellyfin dashboard.
  61. @immutable
  62. class JellyfinAlbum {
  63. const JellyfinAlbum({
  64. required this.id,
  65. required this.name,
  66. required this.artist,
  67. this.year,
  68. this.imageTag,
  69. this.childCount,
  70. this.dateCreated,
  71. this.lastPlayed,
  72. });
  73. final String id;
  74. final String name;
  75. final String artist;
  76. final int? year;
  77. final String? imageTag;
  78. final int? childCount;
  79. final DateTime? dateCreated;
  80. final DateTime? lastPlayed;
  81. bool get hasArtwork => (imageTag?.isNotEmpty ?? false);
  82. factory JellyfinAlbum.fromJson(Map<String, dynamic> json) {
  83. return JellyfinAlbum(
  84. id: _string(json['Id']),
  85. name: _string(json['Name']),
  86. artist: _albumArtist(json),
  87. year: _int(json['ProductionYear']),
  88. imageTag:
  89. _imageTag(json['ImageTags']) ??
  90. _stringOrNull(json['AlbumPrimaryImageTag']),
  91. childCount: _int(json['ChildCount']),
  92. dateCreated: _date(json['DateCreated']),
  93. lastPlayed: _lastPlayed(json['UserData']),
  94. );
  95. }
  96. /// Builds an album card from a played Audio item.
  97. factory JellyfinAlbum.fromTrack(JellyfinTrack track) {
  98. return JellyfinAlbum(
  99. id: track.albumId,
  100. name: track.album.isEmpty ? track.name : track.album,
  101. artist: track.artist,
  102. year: track.year,
  103. imageTag: track.albumImageTag,
  104. lastPlayed: track.lastPlayed,
  105. );
  106. }
  107. /// Builds an album card from a played Audio item. Jellyfin 12.2 updates the
  108. /// track's user data, not the album's, so the recently-played row is built
  109. /// from tracks and deduped by album id.
  110. factory JellyfinAlbum.fromTrackJson(Map<String, dynamic> json) {
  111. final albumId = _string(json['AlbumId']);
  112. return JellyfinAlbum(
  113. id: albumId.isNotEmpty ? albumId : _string(json['ParentId']),
  114. name: _stringOrNull(json['Album']) ?? _string(json['Name']),
  115. artist: _albumArtist(json),
  116. year: _int(json['ProductionYear']),
  117. imageTag:
  118. _stringOrNull(json['AlbumPrimaryImageTag']) ??
  119. _imageTag(json['ImageTags']),
  120. childCount: _int(json['ChildCount']),
  121. dateCreated: _date(json['DateCreated']),
  122. lastPlayed: _lastPlayed(json['UserData']),
  123. );
  124. }
  125. @override
  126. bool operator ==(Object other) {
  127. return other is JellyfinAlbum && other.id == id;
  128. }
  129. @override
  130. int get hashCode => id.hashCode;
  131. @override
  132. String toString() => name;
  133. }
  134. /// A playable audio track. [mediaSourceId] is what Jellyfin playback reporting
  135. /// and the stream endpoint expect; [duration] comes from `RunTimeTicks`.
  136. @immutable
  137. class JellyfinTrack {
  138. const JellyfinTrack({
  139. required this.id,
  140. required this.name,
  141. required this.albumId,
  142. required this.album,
  143. required this.artist,
  144. required this.indexNumber,
  145. required this.parentIndexNumber,
  146. required this.duration,
  147. required this.mediaSourceId,
  148. this.container,
  149. this.codec,
  150. this.genres = const [],
  151. this.lastPlayed,
  152. this.albumImageTag,
  153. this.year,
  154. });
  155. final String id;
  156. final String name;
  157. final String albumId;
  158. final String album;
  159. final String artist;
  160. final int? indexNumber;
  161. final int? parentIndexNumber;
  162. final Duration duration;
  163. final String mediaSourceId;
  164. final String? container;
  165. final String? codec;
  166. final List<String> genres;
  167. final DateTime? lastPlayed;
  168. final String? albumImageTag;
  169. final int? year;
  170. String get displayIndex {
  171. final track = indexNumber;
  172. if (track == null) return '';
  173. final disc = parentIndexNumber;
  174. if (disc != null && disc > 1) return '$disc.$track';
  175. return '$track';
  176. }
  177. factory JellyfinTrack.fromJson(Map<String, dynamic> json) {
  178. final sources = json['MediaSources'];
  179. final source = sources is List && sources.isNotEmpty ? sources.first : null;
  180. final sourceMap = source is Map ? source : null;
  181. final streams = sourceMap?['MediaStreams'];
  182. final stream = streams is List && streams.isNotEmpty ? streams.first : null;
  183. final streamMap = stream is Map ? stream : null;
  184. final ticks = _int(json['RunTimeTicks']) ?? 0;
  185. return JellyfinTrack(
  186. id: _string(json['Id']),
  187. name: _string(json['Name']),
  188. albumId:
  189. _stringOrNull(json['AlbumId']) ??
  190. _stringOrNull(json['ParentId']) ??
  191. '',
  192. album: _string(json['Album']),
  193. artist: _albumArtist(json),
  194. indexNumber: _int(json['IndexNumber']),
  195. parentIndexNumber: _int(json['ParentIndexNumber']),
  196. duration: Duration(microseconds: ticks ~/ 10),
  197. mediaSourceId: _string(sourceMap?['Id']),
  198. container: _stringOrNull(sourceMap?['Container']),
  199. codec: _stringOrNull(streamMap?['Codec']),
  200. genres: _stringList(json['Genres']),
  201. lastPlayed: _lastPlayed(json['UserData']),
  202. albumImageTag: _stringOrNull(json['AlbumPrimaryImageTag']),
  203. year: _int(json['ProductionYear']),
  204. );
  205. }
  206. @override
  207. bool operator ==(Object other) {
  208. return other is JellyfinTrack && other.id == id;
  209. }
  210. @override
  211. int get hashCode => id.hashCode;
  212. @override
  213. String toString() => name;
  214. }
  215. String _string(Object? value) => value is String ? value : '';
  216. String? _stringOrNull(Object? value) {
  217. if (value is! String) return null;
  218. final trimmed = value.trim();
  219. return trimmed.isEmpty ? null : trimmed;
  220. }
  221. int? _int(Object? value) {
  222. if (value is num) return value.toInt();
  223. if (value is String) return int.tryParse(value);
  224. return null;
  225. }
  226. DateTime? _date(Object? value) {
  227. if (value is! String) return null;
  228. return DateTime.tryParse(value);
  229. }
  230. DateTime? _lastPlayed(Object? userData) {
  231. if (userData is! Map) return null;
  232. return _date(userData['LastPlayedDate']);
  233. }
  234. String? _imageTag(Object? imageTags) {
  235. if (imageTags is! Map) return null;
  236. return _stringOrNull(imageTags['Primary']);
  237. }
  238. String _albumArtist(Map<String, dynamic> json) {
  239. final albumArtist = _stringOrNull(json['AlbumArtist']);
  240. if (albumArtist != null) return albumArtist;
  241. final artists = _stringList(json['Artists']);
  242. if (artists.isNotEmpty) return artists.join(', ');
  243. final albumArtists = json['AlbumArtists'];
  244. if (albumArtists is List) {
  245. final names = albumArtists
  246. .whereType<Map>()
  247. .map((entry) => _stringOrNull(entry['Name']))
  248. .whereType<String>()
  249. .toList(growable: false);
  250. if (names.isNotEmpty) return names.join(', ');
  251. }
  252. return '';
  253. }
  254. List<String> _stringList(Object? value) {
  255. if (value is! List) return const [];
  256. return value.whereType<String>().toList(growable: false);
  257. }