| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- import 'package:flutter/foundation.dart';
- /// Streaming quality for Jellyfin audio.
- ///
- /// [direct] sends the original file. Bitrate choices ask Jellyfin to transcode
- /// to MP3 at that cap, which is useful when a car head unit mishandles a
- /// high-resolution source.
- enum JellyfinAudioQuality {
- direct('Direct', null),
- kbps320('320 kbps', 320000),
- kbps256('256 kbps', 256000),
- kbps192('192 kbps', 192000),
- kbps128('128 kbps', 128000);
- const JellyfinAudioQuality(this.label, this.maxStreamingBitrate);
- final String label;
- final int? maxStreamingBitrate;
- bool get isDirect => maxStreamingBitrate == null;
- static JellyfinAudioQuality fromStorage(String? value) {
- return values.firstWhere(
- (quality) => quality.name == value,
- orElse: () => direct,
- );
- }
- }
- /// A saved Jellyfin selection in the genre picker.
- @immutable
- class JellyfinGenreSelection {
- const JellyfinGenreSelection({required this.id, required this.label});
- final String id;
- final String label;
- factory JellyfinGenreSelection.fromJson(Map<String, dynamic> json) {
- return JellyfinGenreSelection(
- id: (json['id'] as String?) ?? '',
- label: (json['label'] as String?) ?? '',
- );
- }
- Map<String, Object?> toJson() => {'id': id, 'label': label};
- @override
- bool operator ==(Object other) {
- return other is JellyfinGenreSelection &&
- other.id == id &&
- other.label == label;
- }
- @override
- int get hashCode => Object.hash(id, label);
- @override
- String toString() => label;
- }
- /// A canonical genre family such as Metal, Rock or Jazz. Raw genres keep their
- /// server spelling; [rawGenres] is what the random query expands to.
- @immutable
- class JellyfinGenreFamily {
- const JellyfinGenreFamily({
- required this.id,
- required this.label,
- required this.rawGenres,
- required this.isStandalone,
- });
- final String id;
- final String label;
- final List<String> rawGenres;
- final bool isStandalone;
- int get rawCount => rawGenres.length;
- @override
- bool operator ==(Object other) {
- return other is JellyfinGenreFamily && other.id == id;
- }
- @override
- int get hashCode => id.hashCode;
- }
- /// One raw genre on the server and the family it belongs to.
- @immutable
- class JellyfinGenre {
- const JellyfinGenre({
- required this.name,
- required this.familyId,
- required this.familyLabel,
- });
- final String name;
- final String familyId;
- final String familyLabel;
- }
- /// An album row in the Jellyfin dashboard.
- @immutable
- class JellyfinAlbum {
- const JellyfinAlbum({
- required this.id,
- required this.name,
- required this.artist,
- this.year,
- this.imageTag,
- this.childCount,
- this.dateCreated,
- this.lastPlayed,
- });
- final String id;
- final String name;
- final String artist;
- final int? year;
- final String? imageTag;
- final int? childCount;
- final DateTime? dateCreated;
- final DateTime? lastPlayed;
- bool get hasArtwork => (imageTag?.isNotEmpty ?? false);
- factory JellyfinAlbum.fromJson(Map<String, dynamic> json) {
- return JellyfinAlbum(
- id: _string(json['Id']),
- name: _string(json['Name']),
- artist: _albumArtist(json),
- year: _int(json['ProductionYear']),
- imageTag:
- _imageTag(json['ImageTags']) ??
- _stringOrNull(json['AlbumPrimaryImageTag']),
- childCount: _int(json['ChildCount']),
- dateCreated: _date(json['DateCreated']),
- lastPlayed: _lastPlayed(json['UserData']),
- );
- }
- /// Builds an album card from a played Audio item.
- factory JellyfinAlbum.fromTrack(JellyfinTrack track) {
- return JellyfinAlbum(
- id: track.albumId,
- name: track.album.isEmpty ? track.name : track.album,
- artist: track.artist,
- year: track.year,
- imageTag: track.albumImageTag,
- lastPlayed: track.lastPlayed,
- );
- }
- /// Builds an album card from a played Audio item. Jellyfin 12.2 updates the
- /// track's user data, not the album's, so the recently-played row is built
- /// from tracks and deduped by album id.
- factory JellyfinAlbum.fromTrackJson(Map<String, dynamic> json) {
- final albumId = _string(json['AlbumId']);
- return JellyfinAlbum(
- id: albumId.isNotEmpty ? albumId : _string(json['ParentId']),
- name: _stringOrNull(json['Album']) ?? _string(json['Name']),
- artist: _albumArtist(json),
- year: _int(json['ProductionYear']),
- imageTag:
- _stringOrNull(json['AlbumPrimaryImageTag']) ??
- _imageTag(json['ImageTags']),
- childCount: _int(json['ChildCount']),
- dateCreated: _date(json['DateCreated']),
- lastPlayed: _lastPlayed(json['UserData']),
- );
- }
- @override
- bool operator ==(Object other) {
- return other is JellyfinAlbum && other.id == id;
- }
- @override
- int get hashCode => id.hashCode;
- @override
- String toString() => name;
- }
- /// A playable audio track. [mediaSourceId] is what Jellyfin playback reporting
- /// and the stream endpoint expect; [duration] comes from `RunTimeTicks`.
- @immutable
- class JellyfinTrack {
- const JellyfinTrack({
- required this.id,
- required this.name,
- required this.albumId,
- required this.album,
- required this.artist,
- required this.indexNumber,
- required this.parentIndexNumber,
- required this.duration,
- required this.mediaSourceId,
- this.container,
- this.codec,
- this.genres = const [],
- this.lastPlayed,
- this.albumImageTag,
- this.year,
- });
- final String id;
- final String name;
- final String albumId;
- final String album;
- final String artist;
- final int? indexNumber;
- final int? parentIndexNumber;
- final Duration duration;
- final String mediaSourceId;
- final String? container;
- final String? codec;
- final List<String> genres;
- final DateTime? lastPlayed;
- final String? albumImageTag;
- final int? year;
- String get displayIndex {
- final track = indexNumber;
- if (track == null) return '';
- final disc = parentIndexNumber;
- if (disc != null && disc > 1) return '$disc.$track';
- return '$track';
- }
- factory JellyfinTrack.fromJson(Map<String, dynamic> json) {
- final sources = json['MediaSources'];
- final source = sources is List && sources.isNotEmpty ? sources.first : null;
- final sourceMap = source is Map ? source : null;
- final streams = sourceMap?['MediaStreams'];
- final stream = streams is List && streams.isNotEmpty ? streams.first : null;
- final streamMap = stream is Map ? stream : null;
- final ticks = _int(json['RunTimeTicks']) ?? 0;
- return JellyfinTrack(
- id: _string(json['Id']),
- name: _string(json['Name']),
- albumId:
- _stringOrNull(json['AlbumId']) ??
- _stringOrNull(json['ParentId']) ??
- '',
- album: _string(json['Album']),
- artist: _albumArtist(json),
- indexNumber: _int(json['IndexNumber']),
- parentIndexNumber: _int(json['ParentIndexNumber']),
- duration: Duration(microseconds: ticks ~/ 10),
- mediaSourceId: _string(sourceMap?['Id']),
- container: _stringOrNull(sourceMap?['Container']),
- codec: _stringOrNull(streamMap?['Codec']),
- genres: _stringList(json['Genres']),
- lastPlayed: _lastPlayed(json['UserData']),
- albumImageTag: _stringOrNull(json['AlbumPrimaryImageTag']),
- year: _int(json['ProductionYear']),
- );
- }
- @override
- bool operator ==(Object other) {
- return other is JellyfinTrack && other.id == id;
- }
- @override
- int get hashCode => id.hashCode;
- @override
- String toString() => name;
- }
- String _string(Object? value) => value is String ? value : '';
- String? _stringOrNull(Object? value) {
- if (value is! String) return null;
- final trimmed = value.trim();
- return trimmed.isEmpty ? null : trimmed;
- }
- int? _int(Object? value) {
- if (value is num) return value.toInt();
- if (value is String) return int.tryParse(value);
- return null;
- }
- DateTime? _date(Object? value) {
- if (value is! String) return null;
- return DateTime.tryParse(value);
- }
- DateTime? _lastPlayed(Object? userData) {
- if (userData is! Map) return null;
- return _date(userData['LastPlayedDate']);
- }
- String? _imageTag(Object? imageTags) {
- if (imageTags is! Map) return null;
- return _stringOrNull(imageTags['Primary']);
- }
- String _albumArtist(Map<String, dynamic> json) {
- final albumArtist = _stringOrNull(json['AlbumArtist']);
- if (albumArtist != null) return albumArtist;
- final artists = _stringList(json['Artists']);
- if (artists.isNotEmpty) return artists.join(', ');
- final albumArtists = json['AlbumArtists'];
- if (albumArtists is List) {
- final names = albumArtists
- .whereType<Map>()
- .map((entry) => _stringOrNull(entry['Name']))
- .whereType<String>()
- .toList(growable: false);
- if (names.isNotEmpty) return names.join(', ');
- }
- return '';
- }
- List<String> _stringList(Object? value) {
- if (value is! List) return const [];
- return value.whereType<String>().toList(growable: false);
- }
|