refactor(media): migrate media item to freezed
This commit is contained in:
+2483
-888
File diff suppressed because it is too large
Load Diff
@@ -14,22 +14,25 @@ MediaFilter _$MediaFilterFromJson(Map<String, dynamic> json) => MediaFilter(
|
||||
type: json['type'] as String? ?? 'filter',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MediaFilterToJson(MediaFilter instance) => <String, dynamic>{
|
||||
'filter': instance.filter,
|
||||
'filterType': instance.filterType,
|
||||
'key': instance.key,
|
||||
'title': instance.title,
|
||||
'type': instance.type,
|
||||
};
|
||||
Map<String, dynamic> _$MediaFilterToJson(MediaFilter instance) =>
|
||||
<String, dynamic>{
|
||||
'filter': instance.filter,
|
||||
'filterType': instance.filterType,
|
||||
'key': instance.key,
|
||||
'title': instance.title,
|
||||
'type': instance.type,
|
||||
};
|
||||
|
||||
MediaFilterValue _$MediaFilterValueFromJson(Map<String, dynamic> json) => MediaFilterValue(
|
||||
key: json['key'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
type: json['type'] as String?,
|
||||
);
|
||||
MediaFilterValue _$MediaFilterValueFromJson(Map<String, dynamic> json) =>
|
||||
MediaFilterValue(
|
||||
key: json['key'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
type: json['type'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MediaFilterValueToJson(MediaFilterValue instance) => <String, dynamic>{
|
||||
'key': instance.key,
|
||||
'title': instance.title,
|
||||
'type': ?instance.type,
|
||||
};
|
||||
Map<String, dynamic> _$MediaFilterValueToJson(MediaFilterValue instance) =>
|
||||
<String, dynamic>{
|
||||
'key': instance.key,
|
||||
'title': instance.title,
|
||||
'type': ?instance.type,
|
||||
};
|
||||
|
||||
+495
-5
@@ -1,13 +1,503 @@
|
||||
// ignore_for_file: invalid_annotation_target
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../services/settings_service.dart' show EpisodePosterMode;
|
||||
import '../utils/global_key_utils.dart';
|
||||
import '../utils/json_utils.dart';
|
||||
import 'media_backend.dart';
|
||||
import 'media_kind.dart';
|
||||
import 'media_part.dart';
|
||||
import 'media_role.dart';
|
||||
import 'media_version.dart';
|
||||
|
||||
part 'media_item/base.dart';
|
||||
part 'media_item/json.dart';
|
||||
part 'media_item/plex.dart';
|
||||
part 'media_item/jellyfin.dart';
|
||||
part 'media_item.freezed.dart';
|
||||
part 'media_item.g.dart';
|
||||
|
||||
/// Backend-neutral media item shape used by UI, providers, persistence, and
|
||||
/// playback. Concrete variants retain backend-only fields without forcing the
|
||||
/// rest of the app to traffic in Plex/Jellyfin DTOs.
|
||||
@Freezed(unionKey: 'backend', unionValueCase: FreezedUnionCase.none, equal: false, makeCollectionsUnmodifiable: false)
|
||||
sealed class MediaItem with _$MediaItem {
|
||||
const MediaItem._();
|
||||
|
||||
/// Backend-dispatching compatibility factory used by existing call sites.
|
||||
factory MediaItem({
|
||||
required String id,
|
||||
required MediaBackend backend,
|
||||
required MediaKind kind,
|
||||
String? guid,
|
||||
String? title,
|
||||
String? titleSort,
|
||||
String? summary,
|
||||
String? tagline,
|
||||
String? originalTitle,
|
||||
String? studio,
|
||||
int? year,
|
||||
String? originallyAvailableAt,
|
||||
String? contentRating,
|
||||
String? parentId,
|
||||
String? parentTitle,
|
||||
String? parentThumbPath,
|
||||
int? parentIndex,
|
||||
int? index,
|
||||
String? grandparentId,
|
||||
String? grandparentTitle,
|
||||
String? grandparentThumbPath,
|
||||
String? grandparentArtPath,
|
||||
String? thumbPath,
|
||||
String? artPath,
|
||||
String? clearLogoPath,
|
||||
String? backgroundSquarePath,
|
||||
int? durationMs,
|
||||
int? viewOffsetMs,
|
||||
int? viewCount,
|
||||
int? lastViewedAt,
|
||||
int? leafCount,
|
||||
int? viewedLeafCount,
|
||||
int? childCount,
|
||||
int? addedAt,
|
||||
int? updatedAt,
|
||||
double? rating,
|
||||
double? userRating,
|
||||
List<String>? genres,
|
||||
List<String>? directors,
|
||||
List<String>? writers,
|
||||
List<String>? producers,
|
||||
List<String>? countries,
|
||||
List<String>? collections,
|
||||
List<String>? labels,
|
||||
List<String>? styles,
|
||||
List<String>? moods,
|
||||
List<MediaRole>? roles,
|
||||
List<MediaVersion>? mediaVersions,
|
||||
String? libraryId,
|
||||
String? libraryTitle,
|
||||
String? audioLanguage,
|
||||
String? subtitleLanguage,
|
||||
int? subtitleMode,
|
||||
String? serverId,
|
||||
String? serverName,
|
||||
Map<String, Object?>? raw,
|
||||
}) {
|
||||
return switch (backend) {
|
||||
MediaBackend.plex => PlexMediaItem(
|
||||
id: id,
|
||||
kind: kind,
|
||||
guid: guid,
|
||||
title: title,
|
||||
titleSort: titleSort,
|
||||
summary: summary,
|
||||
tagline: tagline,
|
||||
originalTitle: originalTitle,
|
||||
studio: studio,
|
||||
year: year,
|
||||
originallyAvailableAt: originallyAvailableAt,
|
||||
contentRating: contentRating,
|
||||
parentId: parentId,
|
||||
parentTitle: parentTitle,
|
||||
parentThumbPath: parentThumbPath,
|
||||
parentIndex: parentIndex,
|
||||
index: index,
|
||||
grandparentId: grandparentId,
|
||||
grandparentTitle: grandparentTitle,
|
||||
grandparentThumbPath: grandparentThumbPath,
|
||||
grandparentArtPath: grandparentArtPath,
|
||||
thumbPath: thumbPath,
|
||||
artPath: artPath,
|
||||
clearLogoPath: clearLogoPath,
|
||||
backgroundSquarePath: backgroundSquarePath,
|
||||
durationMs: durationMs,
|
||||
viewOffsetMs: viewOffsetMs,
|
||||
viewCount: viewCount,
|
||||
lastViewedAt: lastViewedAt,
|
||||
leafCount: leafCount,
|
||||
viewedLeafCount: viewedLeafCount,
|
||||
childCount: childCount,
|
||||
addedAt: addedAt,
|
||||
updatedAt: updatedAt,
|
||||
rating: rating,
|
||||
userRating: userRating,
|
||||
genres: genres,
|
||||
directors: directors,
|
||||
writers: writers,
|
||||
producers: producers,
|
||||
countries: countries,
|
||||
collections: collections,
|
||||
labels: labels,
|
||||
styles: styles,
|
||||
moods: moods,
|
||||
roles: roles,
|
||||
mediaVersions: mediaVersions,
|
||||
libraryId: libraryId,
|
||||
libraryTitle: libraryTitle,
|
||||
audioLanguage: audioLanguage,
|
||||
subtitleLanguage: subtitleLanguage,
|
||||
subtitleMode: subtitleMode,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
raw: raw,
|
||||
),
|
||||
MediaBackend.jellyfin => JellyfinMediaItem(
|
||||
id: id,
|
||||
kind: kind,
|
||||
guid: guid,
|
||||
title: title,
|
||||
titleSort: titleSort,
|
||||
summary: summary,
|
||||
tagline: tagline,
|
||||
originalTitle: originalTitle,
|
||||
studio: studio,
|
||||
year: year,
|
||||
originallyAvailableAt: originallyAvailableAt,
|
||||
contentRating: contentRating,
|
||||
parentId: parentId,
|
||||
parentTitle: parentTitle,
|
||||
parentThumbPath: parentThumbPath,
|
||||
parentIndex: parentIndex,
|
||||
index: index,
|
||||
grandparentId: grandparentId,
|
||||
grandparentTitle: grandparentTitle,
|
||||
grandparentThumbPath: grandparentThumbPath,
|
||||
grandparentArtPath: grandparentArtPath,
|
||||
thumbPath: thumbPath,
|
||||
artPath: artPath,
|
||||
clearLogoPath: clearLogoPath,
|
||||
backgroundSquarePath: backgroundSquarePath,
|
||||
durationMs: durationMs,
|
||||
viewOffsetMs: viewOffsetMs,
|
||||
viewCount: viewCount,
|
||||
lastViewedAt: lastViewedAt,
|
||||
leafCount: leafCount,
|
||||
viewedLeafCount: viewedLeafCount,
|
||||
childCount: childCount,
|
||||
addedAt: addedAt,
|
||||
updatedAt: updatedAt,
|
||||
rating: rating,
|
||||
userRating: userRating,
|
||||
genres: genres,
|
||||
directors: directors,
|
||||
writers: writers,
|
||||
producers: producers,
|
||||
countries: countries,
|
||||
collections: collections,
|
||||
labels: labels,
|
||||
styles: styles,
|
||||
moods: moods,
|
||||
roles: roles,
|
||||
mediaVersions: mediaVersions,
|
||||
libraryId: libraryId,
|
||||
libraryTitle: libraryTitle,
|
||||
audioLanguage: audioLanguage,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
raw: raw,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Backend-tagged concrete subclass for items sourced from a Plex server.
|
||||
@FreezedUnionValue('plex')
|
||||
@JsonSerializable(includeIfNull: false, explicitToJson: true)
|
||||
const factory MediaItem.plex({
|
||||
@JsonKey(readValue: readStringField, defaultValue: '') required String id,
|
||||
@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) required MediaKind kind,
|
||||
String? guid,
|
||||
String? title,
|
||||
String? titleSort,
|
||||
String? summary,
|
||||
String? tagline,
|
||||
String? originalTitle,
|
||||
|
||||
/// Plex `editionTitle` distinguishes versions of the same movie.
|
||||
String? editionTitle,
|
||||
String? studio,
|
||||
@JsonKey(fromJson: flexibleInt) int? year,
|
||||
String? originallyAvailableAt,
|
||||
String? contentRating,
|
||||
String? parentId,
|
||||
String? parentTitle,
|
||||
String? parentThumbPath,
|
||||
@JsonKey(fromJson: flexibleInt) int? parentIndex,
|
||||
@JsonKey(fromJson: flexibleInt) int? index,
|
||||
String? grandparentId,
|
||||
String? grandparentTitle,
|
||||
String? grandparentThumbPath,
|
||||
String? grandparentArtPath,
|
||||
String? thumbPath,
|
||||
String? artPath,
|
||||
String? clearLogoPath,
|
||||
String? backgroundSquarePath,
|
||||
@JsonKey(fromJson: flexibleInt) int? durationMs,
|
||||
@JsonKey(fromJson: flexibleInt) int? viewOffsetMs,
|
||||
@JsonKey(fromJson: flexibleInt) int? viewCount,
|
||||
@JsonKey(fromJson: flexibleInt) int? lastViewedAt,
|
||||
@JsonKey(fromJson: flexibleInt) int? leafCount,
|
||||
@JsonKey(fromJson: flexibleInt) int? viewedLeafCount,
|
||||
@JsonKey(fromJson: flexibleInt) int? childCount,
|
||||
@JsonKey(fromJson: flexibleInt) int? addedAt,
|
||||
@JsonKey(fromJson: flexibleInt) int? updatedAt,
|
||||
@JsonKey(fromJson: flexibleDouble) double? rating,
|
||||
@JsonKey(fromJson: flexibleDouble) double? audienceRating,
|
||||
@JsonKey(fromJson: flexibleDouble) double? userRating,
|
||||
String? ratingImage,
|
||||
String? audienceRatingImage,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? genres,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? directors,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? writers,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? producers,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? countries,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? collections,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? labels,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? styles,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? moods,
|
||||
@JsonKey(fromJson: _mediaItemRolesFromJson) List<MediaRole>? roles,
|
||||
@JsonKey(fromJson: _mediaItemVersionsFromJson) List<MediaVersion>? mediaVersions,
|
||||
String? libraryId,
|
||||
String? libraryTitle,
|
||||
String? audioLanguage,
|
||||
String? subtitleLanguage,
|
||||
@JsonKey(fromJson: flexibleInt) int? subtitleMode,
|
||||
String? trailerKey,
|
||||
@JsonKey(fromJson: flexibleInt) int? playlistItemId,
|
||||
@JsonKey(fromJson: flexibleInt) int? playQueueItemId,
|
||||
String? subtype,
|
||||
@JsonKey(fromJson: flexibleInt) int? extraType,
|
||||
String? serverId,
|
||||
String? serverName,
|
||||
@JsonKey(fromJson: _mediaItemRawFromJson) Map<String, Object?>? raw,
|
||||
}) = PlexMediaItem;
|
||||
|
||||
/// Backend-tagged concrete subclass for items sourced from a Jellyfin server.
|
||||
@FreezedUnionValue('jellyfin')
|
||||
@JsonSerializable(includeIfNull: false, explicitToJson: true)
|
||||
const factory MediaItem.jellyfin({
|
||||
@JsonKey(readValue: readStringField, defaultValue: '') required String id,
|
||||
@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) required MediaKind kind,
|
||||
String? guid,
|
||||
String? title,
|
||||
String? titleSort,
|
||||
String? summary,
|
||||
String? tagline,
|
||||
String? originalTitle,
|
||||
String? studio,
|
||||
@JsonKey(fromJson: flexibleInt) int? year,
|
||||
String? originallyAvailableAt,
|
||||
String? contentRating,
|
||||
String? parentId,
|
||||
String? parentTitle,
|
||||
String? parentThumbPath,
|
||||
@JsonKey(fromJson: flexibleInt) int? parentIndex,
|
||||
@JsonKey(fromJson: flexibleInt) int? index,
|
||||
String? grandparentId,
|
||||
String? grandparentTitle,
|
||||
String? grandparentThumbPath,
|
||||
String? grandparentArtPath,
|
||||
String? thumbPath,
|
||||
String? artPath,
|
||||
String? clearLogoPath,
|
||||
String? backgroundSquarePath,
|
||||
@JsonKey(fromJson: flexibleInt) int? durationMs,
|
||||
@JsonKey(fromJson: flexibleInt) int? viewOffsetMs,
|
||||
@JsonKey(fromJson: flexibleInt) int? viewCount,
|
||||
@JsonKey(fromJson: flexibleInt) int? lastViewedAt,
|
||||
@JsonKey(fromJson: flexibleInt) int? leafCount,
|
||||
@JsonKey(fromJson: flexibleInt) int? viewedLeafCount,
|
||||
@JsonKey(fromJson: flexibleInt) int? childCount,
|
||||
@JsonKey(fromJson: flexibleInt) int? addedAt,
|
||||
@JsonKey(fromJson: flexibleInt) int? updatedAt,
|
||||
@JsonKey(fromJson: flexibleDouble) double? rating,
|
||||
@JsonKey(fromJson: flexibleDouble) double? userRating,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? genres,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? directors,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? writers,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? producers,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? countries,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? collections,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? labels,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? styles,
|
||||
@JsonKey(fromJson: _mediaItemStringList) List<String>? moods,
|
||||
@JsonKey(fromJson: _mediaItemRolesFromJson) List<MediaRole>? roles,
|
||||
@JsonKey(fromJson: _mediaItemVersionsFromJson) List<MediaVersion>? mediaVersions,
|
||||
String? libraryId,
|
||||
String? libraryTitle,
|
||||
String? audioLanguage,
|
||||
|
||||
/// Jellyfin playlist entry id used by playlist write endpoints.
|
||||
String? playlistItemId,
|
||||
String? serverId,
|
||||
String? serverName,
|
||||
@JsonKey(fromJson: _mediaItemRawFromJson) Map<String, Object?>? raw,
|
||||
}) = JellyfinMediaItem;
|
||||
|
||||
MediaBackend get backend => switch (this) {
|
||||
PlexMediaItem() => MediaBackend.plex,
|
||||
JellyfinMediaItem() => MediaBackend.jellyfin,
|
||||
};
|
||||
|
||||
/// Restore a [MediaItem] from a [toJson] payload. Missing/unknown backend
|
||||
/// values use [MediaBackend.fromString] so old offline cache rows remain
|
||||
/// readable instead of throwing before union dispatch.
|
||||
factory MediaItem.fromJson(Map<String, dynamic> json) {
|
||||
return switch (MediaBackend.fromString(json['backend'] as String?)) {
|
||||
MediaBackend.plex => _$PlexMediaItemFromJson(json),
|
||||
MediaBackend.jellyfin => _$JellyfinMediaItemFromJson(json),
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return switch (this) {
|
||||
final PlexMediaItem item => {'backend': MediaBackend.plex.id, ..._$PlexMediaItemToJson(item)},
|
||||
final JellyfinMediaItem item => {'backend': MediaBackend.jellyfin.id, ..._$JellyfinMediaItemToJson(item)},
|
||||
};
|
||||
}
|
||||
|
||||
/// Global unique identifier across all servers (`serverId:id`). Falls back
|
||||
/// to bare [id] if [serverId] is missing.
|
||||
String get globalKey => serverId != null ? buildGlobalKey(serverId!, id) : id;
|
||||
|
||||
/// Global unique identifier of this item's library section.
|
||||
String? get libraryGlobalKey => serverId != null && libraryId != null ? buildGlobalKey(serverId!, libraryId!) : null;
|
||||
|
||||
/// Parent rating keys for hierarchical invalidation. For an episode:
|
||||
/// `[seasonId, showId]`. For a season: `[showId]`. For a movie: `[]`.
|
||||
List<String> get parentChain => [?parentId, ?grandparentId];
|
||||
|
||||
/// Whether this item has started but not finished playback.
|
||||
bool get hasActiveProgress {
|
||||
if (durationMs == null || viewOffsetMs == null) return false;
|
||||
return viewOffsetMs! > 0 && viewOffsetMs! < durationMs!;
|
||||
}
|
||||
|
||||
/// Whether this container (show/season) has some but not all leaves watched.
|
||||
bool get isPartiallyWatched =>
|
||||
viewedLeafCount != null && leafCount != null && viewedLeafCount! > 0 && viewedLeafCount! < leafCount!;
|
||||
|
||||
/// Whether the item is fully watched. Series/seasons consult leaf counts;
|
||||
/// individual movies/episodes use [viewCount].
|
||||
bool get isWatched {
|
||||
if (leafCount != null && viewedLeafCount != null) {
|
||||
return viewedLeafCount! >= leafCount!;
|
||||
}
|
||||
return viewCount != null && viewCount! > 0;
|
||||
}
|
||||
|
||||
/// Display-friendly title that prefers the show name for episodes/seasons.
|
||||
String get displayTitle {
|
||||
if ((kind == MediaKind.episode || kind == MediaKind.season) && grandparentTitle != null) {
|
||||
return grandparentTitle!;
|
||||
}
|
||||
if (kind == MediaKind.season && parentTitle != null) {
|
||||
return parentTitle!;
|
||||
}
|
||||
return title ?? '';
|
||||
}
|
||||
|
||||
/// Subtitle line shown below [displayTitle] for episodes/seasons.
|
||||
String? get displaySubtitle {
|
||||
if (kind == MediaKind.episode || kind == MediaKind.season) {
|
||||
if (grandparentTitle != null || (kind == MediaKind.season && parentTitle != null)) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Plex-only edition label. Jellyfin returns null.
|
||||
String? get editionTitle => null;
|
||||
|
||||
/// Returns the appropriate poster path based on episode poster mode.
|
||||
String? posterThumb({EpisodePosterMode mode = EpisodePosterMode.seriesPoster, bool mixedHubContext = false}) {
|
||||
if (kind == MediaKind.episode) {
|
||||
switch (mode) {
|
||||
case EpisodePosterMode.episodeThumbnail:
|
||||
return thumbPath;
|
||||
case EpisodePosterMode.seasonPoster:
|
||||
return parentThumbPath ?? grandparentThumbPath ?? thumbPath;
|
||||
case EpisodePosterMode.seriesPoster:
|
||||
return grandparentThumbPath ?? thumbPath;
|
||||
}
|
||||
} else if (kind == MediaKind.season) {
|
||||
if (mixedHubContext && mode == EpisodePosterMode.episodeThumbnail) {
|
||||
return artPath ?? thumbPath;
|
||||
}
|
||||
if (grandparentThumbPath != null) {
|
||||
return grandparentThumbPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (mixedHubContext &&
|
||||
mode == EpisodePosterMode.episodeThumbnail &&
|
||||
(kind == MediaKind.movie || kind == MediaKind.show)) {
|
||||
return artPath ?? thumbPath;
|
||||
}
|
||||
|
||||
return thumbPath;
|
||||
}
|
||||
|
||||
/// Secondary poster path to try when [posterThumb] returns an image URL that
|
||||
/// exists syntactically but the server cannot serve it.
|
||||
String? posterThumbFallback({EpisodePosterMode mode = EpisodePosterMode.seriesPoster, bool mixedHubContext = false}) {
|
||||
if (kind != MediaKind.episode || mode != EpisodePosterMode.seasonPoster) return null;
|
||||
final fallback = grandparentThumbPath ?? thumbPath;
|
||||
return fallback != null && fallback != posterThumb(mode: mode, mixedHubContext: mixedHubContext) ? fallback : null;
|
||||
}
|
||||
|
||||
/// True when the item should render in 16:9.
|
||||
bool usesWideAspectRatio(EpisodePosterMode mode, {bool mixedHubContext = false}) {
|
||||
if (kind == MediaKind.clip) return true;
|
||||
if (kind == MediaKind.episode && mode == EpisodePosterMode.episodeThumbnail) {
|
||||
return true;
|
||||
}
|
||||
if (mixedHubContext &&
|
||||
mode == EpisodePosterMode.episodeThumbnail &&
|
||||
(kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.season)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns the best hero art path based on the container's aspect ratio.
|
||||
String? heroArt({required double containerAspectRatio}) {
|
||||
final candidates = heroArtCandidates(containerAspectRatio: containerAspectRatio);
|
||||
if (candidates.isEmpty) return null;
|
||||
return candidates.first;
|
||||
}
|
||||
|
||||
/// Returns hero art candidates in display-preference order.
|
||||
List<String> heroArtCandidates({required double containerAspectRatio}) {
|
||||
final preferred = containerAspectRatio < 1.39 ? [backgroundSquarePath, artPath] : [artPath, backgroundSquarePath];
|
||||
|
||||
final candidates = <String>[];
|
||||
for (final path in preferred) {
|
||||
if (path == null || path.isEmpty || candidates.contains(path)) continue;
|
||||
candidates.add(path);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
}
|
||||
|
||||
MediaKind _mediaKindFromJson(Object? raw) => MediaKind.fromString(raw as String?);
|
||||
|
||||
String _mediaKindToJson(MediaKind kind) => kind.id;
|
||||
|
||||
List<String>? _mediaItemStringList(Object? raw) => stringListFromRaw(raw, stringify: true);
|
||||
|
||||
List<MediaRole>? _mediaItemRolesFromJson(Object? raw) {
|
||||
return raw is List
|
||||
? [
|
||||
for (final role in raw)
|
||||
if (role is Map<String, dynamic>) MediaRole.fromJson(role),
|
||||
]
|
||||
: null;
|
||||
}
|
||||
|
||||
List<MediaVersion>? _mediaItemVersionsFromJson(Object? raw) {
|
||||
return raw is List
|
||||
? [
|
||||
for (final version in raw)
|
||||
if (version is Map<String, dynamic>) MediaVersion.fromJson(version),
|
||||
]
|
||||
: null;
|
||||
}
|
||||
|
||||
Map<String, Object?>? _mediaItemRawFromJson(Object? raw) => raw is Map ? Map<String, Object?>.from(raw) : null;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,259 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'media_item.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
PlexMediaItem _$PlexMediaItemFromJson(Map<String, dynamic> json) =>
|
||||
PlexMediaItem(
|
||||
id: readStringField(json, 'id') as String? ?? '',
|
||||
kind: _mediaKindFromJson(json['kind']),
|
||||
guid: json['guid'] as String?,
|
||||
title: json['title'] as String?,
|
||||
titleSort: json['titleSort'] as String?,
|
||||
summary: json['summary'] as String?,
|
||||
tagline: json['tagline'] as String?,
|
||||
originalTitle: json['originalTitle'] as String?,
|
||||
editionTitle: json['editionTitle'] as String?,
|
||||
studio: json['studio'] as String?,
|
||||
year: flexibleInt(json['year']),
|
||||
originallyAvailableAt: json['originallyAvailableAt'] as String?,
|
||||
contentRating: json['contentRating'] as String?,
|
||||
parentId: json['parentId'] as String?,
|
||||
parentTitle: json['parentTitle'] as String?,
|
||||
parentThumbPath: json['parentThumbPath'] as String?,
|
||||
parentIndex: flexibleInt(json['parentIndex']),
|
||||
index: flexibleInt(json['index']),
|
||||
grandparentId: json['grandparentId'] as String?,
|
||||
grandparentTitle: json['grandparentTitle'] as String?,
|
||||
grandparentThumbPath: json['grandparentThumbPath'] as String?,
|
||||
grandparentArtPath: json['grandparentArtPath'] as String?,
|
||||
thumbPath: json['thumbPath'] as String?,
|
||||
artPath: json['artPath'] as String?,
|
||||
clearLogoPath: json['clearLogoPath'] as String?,
|
||||
backgroundSquarePath: json['backgroundSquarePath'] as String?,
|
||||
durationMs: flexibleInt(json['durationMs']),
|
||||
viewOffsetMs: flexibleInt(json['viewOffsetMs']),
|
||||
viewCount: flexibleInt(json['viewCount']),
|
||||
lastViewedAt: flexibleInt(json['lastViewedAt']),
|
||||
leafCount: flexibleInt(json['leafCount']),
|
||||
viewedLeafCount: flexibleInt(json['viewedLeafCount']),
|
||||
childCount: flexibleInt(json['childCount']),
|
||||
addedAt: flexibleInt(json['addedAt']),
|
||||
updatedAt: flexibleInt(json['updatedAt']),
|
||||
rating: flexibleDouble(json['rating']),
|
||||
audienceRating: flexibleDouble(json['audienceRating']),
|
||||
userRating: flexibleDouble(json['userRating']),
|
||||
ratingImage: json['ratingImage'] as String?,
|
||||
audienceRatingImage: json['audienceRatingImage'] as String?,
|
||||
genres: _mediaItemStringList(json['genres']),
|
||||
directors: _mediaItemStringList(json['directors']),
|
||||
writers: _mediaItemStringList(json['writers']),
|
||||
producers: _mediaItemStringList(json['producers']),
|
||||
countries: _mediaItemStringList(json['countries']),
|
||||
collections: _mediaItemStringList(json['collections']),
|
||||
labels: _mediaItemStringList(json['labels']),
|
||||
styles: _mediaItemStringList(json['styles']),
|
||||
moods: _mediaItemStringList(json['moods']),
|
||||
roles: _mediaItemRolesFromJson(json['roles']),
|
||||
mediaVersions: _mediaItemVersionsFromJson(json['mediaVersions']),
|
||||
libraryId: json['libraryId'] as String?,
|
||||
libraryTitle: json['libraryTitle'] as String?,
|
||||
audioLanguage: json['audioLanguage'] as String?,
|
||||
subtitleLanguage: json['subtitleLanguage'] as String?,
|
||||
subtitleMode: flexibleInt(json['subtitleMode']),
|
||||
trailerKey: json['trailerKey'] as String?,
|
||||
playlistItemId: flexibleInt(json['playlistItemId']),
|
||||
playQueueItemId: flexibleInt(json['playQueueItemId']),
|
||||
subtype: json['subtype'] as String?,
|
||||
extraType: flexibleInt(json['extraType']),
|
||||
serverId: json['serverId'] as String?,
|
||||
serverName: json['serverName'] as String?,
|
||||
raw: _mediaItemRawFromJson(json['raw']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlexMediaItemToJson(PlexMediaItem instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'kind': _mediaKindToJson(instance.kind),
|
||||
'guid': ?instance.guid,
|
||||
'title': ?instance.title,
|
||||
'titleSort': ?instance.titleSort,
|
||||
'summary': ?instance.summary,
|
||||
'tagline': ?instance.tagline,
|
||||
'originalTitle': ?instance.originalTitle,
|
||||
'editionTitle': ?instance.editionTitle,
|
||||
'studio': ?instance.studio,
|
||||
'year': ?instance.year,
|
||||
'originallyAvailableAt': ?instance.originallyAvailableAt,
|
||||
'contentRating': ?instance.contentRating,
|
||||
'parentId': ?instance.parentId,
|
||||
'parentTitle': ?instance.parentTitle,
|
||||
'parentThumbPath': ?instance.parentThumbPath,
|
||||
'parentIndex': ?instance.parentIndex,
|
||||
'index': ?instance.index,
|
||||
'grandparentId': ?instance.grandparentId,
|
||||
'grandparentTitle': ?instance.grandparentTitle,
|
||||
'grandparentThumbPath': ?instance.grandparentThumbPath,
|
||||
'grandparentArtPath': ?instance.grandparentArtPath,
|
||||
'thumbPath': ?instance.thumbPath,
|
||||
'artPath': ?instance.artPath,
|
||||
'clearLogoPath': ?instance.clearLogoPath,
|
||||
'backgroundSquarePath': ?instance.backgroundSquarePath,
|
||||
'durationMs': ?instance.durationMs,
|
||||
'viewOffsetMs': ?instance.viewOffsetMs,
|
||||
'viewCount': ?instance.viewCount,
|
||||
'lastViewedAt': ?instance.lastViewedAt,
|
||||
'leafCount': ?instance.leafCount,
|
||||
'viewedLeafCount': ?instance.viewedLeafCount,
|
||||
'childCount': ?instance.childCount,
|
||||
'addedAt': ?instance.addedAt,
|
||||
'updatedAt': ?instance.updatedAt,
|
||||
'rating': ?instance.rating,
|
||||
'audienceRating': ?instance.audienceRating,
|
||||
'userRating': ?instance.userRating,
|
||||
'ratingImage': ?instance.ratingImage,
|
||||
'audienceRatingImage': ?instance.audienceRatingImage,
|
||||
'genres': ?instance.genres,
|
||||
'directors': ?instance.directors,
|
||||
'writers': ?instance.writers,
|
||||
'producers': ?instance.producers,
|
||||
'countries': ?instance.countries,
|
||||
'collections': ?instance.collections,
|
||||
'labels': ?instance.labels,
|
||||
'styles': ?instance.styles,
|
||||
'moods': ?instance.moods,
|
||||
'roles': ?instance.roles?.map((e) => e.toJson()).toList(),
|
||||
'mediaVersions': ?instance.mediaVersions?.map((e) => e.toJson()).toList(),
|
||||
'libraryId': ?instance.libraryId,
|
||||
'libraryTitle': ?instance.libraryTitle,
|
||||
'audioLanguage': ?instance.audioLanguage,
|
||||
'subtitleLanguage': ?instance.subtitleLanguage,
|
||||
'subtitleMode': ?instance.subtitleMode,
|
||||
'trailerKey': ?instance.trailerKey,
|
||||
'playlistItemId': ?instance.playlistItemId,
|
||||
'playQueueItemId': ?instance.playQueueItemId,
|
||||
'subtype': ?instance.subtype,
|
||||
'extraType': ?instance.extraType,
|
||||
'serverId': ?instance.serverId,
|
||||
'serverName': ?instance.serverName,
|
||||
'raw': ?instance.raw,
|
||||
};
|
||||
|
||||
JellyfinMediaItem _$JellyfinMediaItemFromJson(Map<String, dynamic> json) =>
|
||||
JellyfinMediaItem(
|
||||
id: readStringField(json, 'id') as String? ?? '',
|
||||
kind: _mediaKindFromJson(json['kind']),
|
||||
guid: json['guid'] as String?,
|
||||
title: json['title'] as String?,
|
||||
titleSort: json['titleSort'] as String?,
|
||||
summary: json['summary'] as String?,
|
||||
tagline: json['tagline'] as String?,
|
||||
originalTitle: json['originalTitle'] as String?,
|
||||
studio: json['studio'] as String?,
|
||||
year: flexibleInt(json['year']),
|
||||
originallyAvailableAt: json['originallyAvailableAt'] as String?,
|
||||
contentRating: json['contentRating'] as String?,
|
||||
parentId: json['parentId'] as String?,
|
||||
parentTitle: json['parentTitle'] as String?,
|
||||
parentThumbPath: json['parentThumbPath'] as String?,
|
||||
parentIndex: flexibleInt(json['parentIndex']),
|
||||
index: flexibleInt(json['index']),
|
||||
grandparentId: json['grandparentId'] as String?,
|
||||
grandparentTitle: json['grandparentTitle'] as String?,
|
||||
grandparentThumbPath: json['grandparentThumbPath'] as String?,
|
||||
grandparentArtPath: json['grandparentArtPath'] as String?,
|
||||
thumbPath: json['thumbPath'] as String?,
|
||||
artPath: json['artPath'] as String?,
|
||||
clearLogoPath: json['clearLogoPath'] as String?,
|
||||
backgroundSquarePath: json['backgroundSquarePath'] as String?,
|
||||
durationMs: flexibleInt(json['durationMs']),
|
||||
viewOffsetMs: flexibleInt(json['viewOffsetMs']),
|
||||
viewCount: flexibleInt(json['viewCount']),
|
||||
lastViewedAt: flexibleInt(json['lastViewedAt']),
|
||||
leafCount: flexibleInt(json['leafCount']),
|
||||
viewedLeafCount: flexibleInt(json['viewedLeafCount']),
|
||||
childCount: flexibleInt(json['childCount']),
|
||||
addedAt: flexibleInt(json['addedAt']),
|
||||
updatedAt: flexibleInt(json['updatedAt']),
|
||||
rating: flexibleDouble(json['rating']),
|
||||
userRating: flexibleDouble(json['userRating']),
|
||||
genres: _mediaItemStringList(json['genres']),
|
||||
directors: _mediaItemStringList(json['directors']),
|
||||
writers: _mediaItemStringList(json['writers']),
|
||||
producers: _mediaItemStringList(json['producers']),
|
||||
countries: _mediaItemStringList(json['countries']),
|
||||
collections: _mediaItemStringList(json['collections']),
|
||||
labels: _mediaItemStringList(json['labels']),
|
||||
styles: _mediaItemStringList(json['styles']),
|
||||
moods: _mediaItemStringList(json['moods']),
|
||||
roles: _mediaItemRolesFromJson(json['roles']),
|
||||
mediaVersions: _mediaItemVersionsFromJson(json['mediaVersions']),
|
||||
libraryId: json['libraryId'] as String?,
|
||||
libraryTitle: json['libraryTitle'] as String?,
|
||||
audioLanguage: json['audioLanguage'] as String?,
|
||||
playlistItemId: json['playlistItemId'] as String?,
|
||||
serverId: json['serverId'] as String?,
|
||||
serverName: json['serverName'] as String?,
|
||||
raw: _mediaItemRawFromJson(json['raw']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$JellyfinMediaItemToJson(JellyfinMediaItem instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'kind': _mediaKindToJson(instance.kind),
|
||||
'guid': ?instance.guid,
|
||||
'title': ?instance.title,
|
||||
'titleSort': ?instance.titleSort,
|
||||
'summary': ?instance.summary,
|
||||
'tagline': ?instance.tagline,
|
||||
'originalTitle': ?instance.originalTitle,
|
||||
'studio': ?instance.studio,
|
||||
'year': ?instance.year,
|
||||
'originallyAvailableAt': ?instance.originallyAvailableAt,
|
||||
'contentRating': ?instance.contentRating,
|
||||
'parentId': ?instance.parentId,
|
||||
'parentTitle': ?instance.parentTitle,
|
||||
'parentThumbPath': ?instance.parentThumbPath,
|
||||
'parentIndex': ?instance.parentIndex,
|
||||
'index': ?instance.index,
|
||||
'grandparentId': ?instance.grandparentId,
|
||||
'grandparentTitle': ?instance.grandparentTitle,
|
||||
'grandparentThumbPath': ?instance.grandparentThumbPath,
|
||||
'grandparentArtPath': ?instance.grandparentArtPath,
|
||||
'thumbPath': ?instance.thumbPath,
|
||||
'artPath': ?instance.artPath,
|
||||
'clearLogoPath': ?instance.clearLogoPath,
|
||||
'backgroundSquarePath': ?instance.backgroundSquarePath,
|
||||
'durationMs': ?instance.durationMs,
|
||||
'viewOffsetMs': ?instance.viewOffsetMs,
|
||||
'viewCount': ?instance.viewCount,
|
||||
'lastViewedAt': ?instance.lastViewedAt,
|
||||
'leafCount': ?instance.leafCount,
|
||||
'viewedLeafCount': ?instance.viewedLeafCount,
|
||||
'childCount': ?instance.childCount,
|
||||
'addedAt': ?instance.addedAt,
|
||||
'updatedAt': ?instance.updatedAt,
|
||||
'rating': ?instance.rating,
|
||||
'userRating': ?instance.userRating,
|
||||
'genres': ?instance.genres,
|
||||
'directors': ?instance.directors,
|
||||
'writers': ?instance.writers,
|
||||
'producers': ?instance.producers,
|
||||
'countries': ?instance.countries,
|
||||
'collections': ?instance.collections,
|
||||
'labels': ?instance.labels,
|
||||
'styles': ?instance.styles,
|
||||
'moods': ?instance.moods,
|
||||
'roles': ?instance.roles?.map((e) => e.toJson()).toList(),
|
||||
'mediaVersions': ?instance.mediaVersions?.map((e) => e.toJson()).toList(),
|
||||
'libraryId': ?instance.libraryId,
|
||||
'libraryTitle': ?instance.libraryTitle,
|
||||
'audioLanguage': ?instance.audioLanguage,
|
||||
'playlistItemId': ?instance.playlistItemId,
|
||||
'serverId': ?instance.serverId,
|
||||
'serverName': ?instance.serverName,
|
||||
'raw': ?instance.raw,
|
||||
};
|
||||
@@ -1,682 +0,0 @@
|
||||
part of '../media_item.dart';
|
||||
|
||||
/// Backend-neutral media item — the central domain type the app's UI,
|
||||
/// providers, and persistence layer operate on. Each backend's adapter is
|
||||
/// responsible for mapping its native representation (Plex `Metadata`,
|
||||
/// Jellyfin `BaseItemDto`) into this shape.
|
||||
///
|
||||
/// Sealed root with two concrete subclasses: [PlexMediaItem] (carries
|
||||
/// Plex-only fields like `trailerKey`, `playQueueItemId`, `audienceRating`)
|
||||
/// and [JellyfinMediaItem] (only the backend-neutral fields). Read sites
|
||||
/// that need a Plex-only field type-narrow with
|
||||
/// `case PlexMediaItem(:final trailerKey?)` or
|
||||
/// `if (item is PlexMediaItem) item.trailerKey`.
|
||||
sealed class MediaItem {
|
||||
/// Backend-opaque identifier (Plex `ratingKey`, Jellyfin `Id`).
|
||||
final String id;
|
||||
final MediaBackend backend;
|
||||
final MediaKind kind;
|
||||
|
||||
/// Stable cross-backend identifier (Plex `guid`, Jellyfin `Id` URI). Used
|
||||
/// for matching across servers and for Trakt-style external lookups.
|
||||
final String? guid;
|
||||
|
||||
final String? title;
|
||||
final String? titleSort;
|
||||
final String? summary;
|
||||
final String? tagline;
|
||||
final String? originalTitle;
|
||||
final String? studio;
|
||||
final int? year;
|
||||
|
||||
/// Original release date (`YYYY-MM-DD`).
|
||||
final String? originallyAvailableAt;
|
||||
final String? contentRating;
|
||||
|
||||
final String? parentId;
|
||||
final String? parentTitle;
|
||||
final String? parentThumbPath;
|
||||
final int? parentIndex;
|
||||
final int? index;
|
||||
final String? grandparentId;
|
||||
final String? grandparentTitle;
|
||||
final String? grandparentThumbPath;
|
||||
final String? grandparentArtPath;
|
||||
|
||||
final String? thumbPath;
|
||||
final String? artPath;
|
||||
final String? clearLogoPath;
|
||||
final String? backgroundSquarePath;
|
||||
|
||||
final int? durationMs;
|
||||
|
||||
/// Resume position in ms.
|
||||
final int? viewOffsetMs;
|
||||
final int? viewCount;
|
||||
final int? lastViewedAt;
|
||||
|
||||
/// Total leaf items (episodes in a show/season, items in a collection).
|
||||
final int? leafCount;
|
||||
|
||||
/// Watched leaf items.
|
||||
final int? viewedLeafCount;
|
||||
|
||||
/// Direct children count (e.g. seasons in a show).
|
||||
final int? childCount;
|
||||
|
||||
final int? addedAt;
|
||||
final int? updatedAt;
|
||||
|
||||
final double? rating;
|
||||
final double? userRating;
|
||||
|
||||
final List<String>? genres;
|
||||
final List<String>? directors;
|
||||
final List<String>? writers;
|
||||
final List<String>? producers;
|
||||
final List<String>? countries;
|
||||
final List<String>? collections;
|
||||
final List<String>? labels;
|
||||
final List<String>? styles;
|
||||
final List<String>? moods;
|
||||
final List<MediaRole>? roles;
|
||||
|
||||
final List<MediaVersion>? mediaVersions;
|
||||
|
||||
/// Backend-opaque library/section id this item belongs to.
|
||||
final String? libraryId;
|
||||
final String? libraryTitle;
|
||||
|
||||
/// Preferred audio language for this item — used by track-selection
|
||||
/// fallback (Priority 3) on both backends. Plex persists changes via
|
||||
/// [PlexClient.setMetadataPreferences]; Jellyfin populates it from the
|
||||
/// per-user `PreferredMetadataLanguage` field but has no per-item write
|
||||
/// endpoint, so the value is read-only there.
|
||||
final String? audioLanguage;
|
||||
|
||||
final String? serverId;
|
||||
final String? serverName;
|
||||
|
||||
/// Untyped fall-through for backend-specific fields not yet mapped onto a
|
||||
/// typed accessor. Use sparingly; promote to typed fields when stable.
|
||||
final Map<String, Object?>? raw;
|
||||
|
||||
const MediaItem._({
|
||||
required this.id,
|
||||
required this.backend,
|
||||
required this.kind,
|
||||
this.guid,
|
||||
this.title,
|
||||
this.titleSort,
|
||||
this.summary,
|
||||
this.tagline,
|
||||
this.originalTitle,
|
||||
this.studio,
|
||||
this.year,
|
||||
this.originallyAvailableAt,
|
||||
this.contentRating,
|
||||
this.parentId,
|
||||
this.parentTitle,
|
||||
this.parentThumbPath,
|
||||
this.parentIndex,
|
||||
this.index,
|
||||
this.grandparentId,
|
||||
this.grandparentTitle,
|
||||
this.grandparentThumbPath,
|
||||
this.grandparentArtPath,
|
||||
this.thumbPath,
|
||||
this.artPath,
|
||||
this.clearLogoPath,
|
||||
this.backgroundSquarePath,
|
||||
this.durationMs,
|
||||
this.viewOffsetMs,
|
||||
this.viewCount,
|
||||
this.lastViewedAt,
|
||||
this.leafCount,
|
||||
this.viewedLeafCount,
|
||||
this.childCount,
|
||||
this.addedAt,
|
||||
this.updatedAt,
|
||||
this.rating,
|
||||
this.userRating,
|
||||
this.genres,
|
||||
this.directors,
|
||||
this.writers,
|
||||
this.producers,
|
||||
this.countries,
|
||||
this.collections,
|
||||
this.labels,
|
||||
this.styles,
|
||||
this.moods,
|
||||
this.roles,
|
||||
this.mediaVersions,
|
||||
this.libraryId,
|
||||
this.libraryTitle,
|
||||
this.audioLanguage,
|
||||
this.serverId,
|
||||
this.serverName,
|
||||
this.raw,
|
||||
});
|
||||
|
||||
/// Backend-dispatching factory: constructs the right concrete subclass
|
||||
/// for the given [backend].
|
||||
factory MediaItem({
|
||||
required String id,
|
||||
required MediaBackend backend,
|
||||
required MediaKind kind,
|
||||
String? guid,
|
||||
String? title,
|
||||
String? titleSort,
|
||||
String? summary,
|
||||
String? tagline,
|
||||
String? originalTitle,
|
||||
String? studio,
|
||||
int? year,
|
||||
String? originallyAvailableAt,
|
||||
String? contentRating,
|
||||
String? parentId,
|
||||
String? parentTitle,
|
||||
String? parentThumbPath,
|
||||
int? parentIndex,
|
||||
int? index,
|
||||
String? grandparentId,
|
||||
String? grandparentTitle,
|
||||
String? grandparentThumbPath,
|
||||
String? grandparentArtPath,
|
||||
String? thumbPath,
|
||||
String? artPath,
|
||||
String? clearLogoPath,
|
||||
String? backgroundSquarePath,
|
||||
int? durationMs,
|
||||
int? viewOffsetMs,
|
||||
int? viewCount,
|
||||
int? lastViewedAt,
|
||||
int? leafCount,
|
||||
int? viewedLeafCount,
|
||||
int? childCount,
|
||||
int? addedAt,
|
||||
int? updatedAt,
|
||||
double? rating,
|
||||
double? userRating,
|
||||
List<String>? genres,
|
||||
List<String>? directors,
|
||||
List<String>? writers,
|
||||
List<String>? producers,
|
||||
List<String>? countries,
|
||||
List<String>? collections,
|
||||
List<String>? labels,
|
||||
List<String>? styles,
|
||||
List<String>? moods,
|
||||
List<MediaRole>? roles,
|
||||
List<MediaVersion>? mediaVersions,
|
||||
String? libraryId,
|
||||
String? libraryTitle,
|
||||
String? audioLanguage,
|
||||
|
||||
/// Plex-only — silently ignored when [backend] is Jellyfin (Jellyfin has
|
||||
/// no per-item subtitle preference write endpoint). Forwarded to
|
||||
/// [PlexMediaItem] only.
|
||||
String? subtitleLanguage,
|
||||
int? subtitleMode,
|
||||
String? serverId,
|
||||
String? serverName,
|
||||
Map<String, Object?>? raw,
|
||||
}) {
|
||||
return switch (backend) {
|
||||
MediaBackend.plex => PlexMediaItem(
|
||||
id: id,
|
||||
kind: kind,
|
||||
guid: guid,
|
||||
title: title,
|
||||
titleSort: titleSort,
|
||||
summary: summary,
|
||||
tagline: tagline,
|
||||
originalTitle: originalTitle,
|
||||
studio: studio,
|
||||
year: year,
|
||||
originallyAvailableAt: originallyAvailableAt,
|
||||
contentRating: contentRating,
|
||||
parentId: parentId,
|
||||
parentTitle: parentTitle,
|
||||
parentThumbPath: parentThumbPath,
|
||||
parentIndex: parentIndex,
|
||||
index: index,
|
||||
grandparentId: grandparentId,
|
||||
grandparentTitle: grandparentTitle,
|
||||
grandparentThumbPath: grandparentThumbPath,
|
||||
grandparentArtPath: grandparentArtPath,
|
||||
thumbPath: thumbPath,
|
||||
artPath: artPath,
|
||||
clearLogoPath: clearLogoPath,
|
||||
backgroundSquarePath: backgroundSquarePath,
|
||||
durationMs: durationMs,
|
||||
viewOffsetMs: viewOffsetMs,
|
||||
viewCount: viewCount,
|
||||
lastViewedAt: lastViewedAt,
|
||||
leafCount: leafCount,
|
||||
viewedLeafCount: viewedLeafCount,
|
||||
childCount: childCount,
|
||||
addedAt: addedAt,
|
||||
updatedAt: updatedAt,
|
||||
rating: rating,
|
||||
userRating: userRating,
|
||||
genres: genres,
|
||||
directors: directors,
|
||||
writers: writers,
|
||||
producers: producers,
|
||||
countries: countries,
|
||||
collections: collections,
|
||||
labels: labels,
|
||||
styles: styles,
|
||||
moods: moods,
|
||||
roles: roles,
|
||||
mediaVersions: mediaVersions,
|
||||
libraryId: libraryId,
|
||||
libraryTitle: libraryTitle,
|
||||
audioLanguage: audioLanguage,
|
||||
subtitleLanguage: subtitleLanguage,
|
||||
subtitleMode: subtitleMode,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
raw: raw,
|
||||
),
|
||||
MediaBackend.jellyfin => JellyfinMediaItem(
|
||||
id: id,
|
||||
kind: kind,
|
||||
guid: guid,
|
||||
title: title,
|
||||
titleSort: titleSort,
|
||||
summary: summary,
|
||||
tagline: tagline,
|
||||
originalTitle: originalTitle,
|
||||
studio: studio,
|
||||
year: year,
|
||||
originallyAvailableAt: originallyAvailableAt,
|
||||
contentRating: contentRating,
|
||||
parentId: parentId,
|
||||
parentTitle: parentTitle,
|
||||
parentThumbPath: parentThumbPath,
|
||||
parentIndex: parentIndex,
|
||||
index: index,
|
||||
grandparentId: grandparentId,
|
||||
grandparentTitle: grandparentTitle,
|
||||
grandparentThumbPath: grandparentThumbPath,
|
||||
grandparentArtPath: grandparentArtPath,
|
||||
thumbPath: thumbPath,
|
||||
artPath: artPath,
|
||||
clearLogoPath: clearLogoPath,
|
||||
backgroundSquarePath: backgroundSquarePath,
|
||||
durationMs: durationMs,
|
||||
viewOffsetMs: viewOffsetMs,
|
||||
viewCount: viewCount,
|
||||
lastViewedAt: lastViewedAt,
|
||||
leafCount: leafCount,
|
||||
viewedLeafCount: viewedLeafCount,
|
||||
childCount: childCount,
|
||||
addedAt: addedAt,
|
||||
updatedAt: updatedAt,
|
||||
rating: rating,
|
||||
userRating: userRating,
|
||||
genres: genres,
|
||||
directors: directors,
|
||||
writers: writers,
|
||||
producers: producers,
|
||||
countries: countries,
|
||||
collections: collections,
|
||||
labels: labels,
|
||||
styles: styles,
|
||||
moods: moods,
|
||||
roles: roles,
|
||||
mediaVersions: mediaVersions,
|
||||
libraryId: libraryId,
|
||||
libraryTitle: libraryTitle,
|
||||
audioLanguage: audioLanguage,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
raw: raw,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Global unique identifier across all servers (`serverId:id`). Falls back
|
||||
/// to bare [id] if [serverId] is missing.
|
||||
String get globalKey => serverId != null ? buildGlobalKey(serverId!, id) : id;
|
||||
|
||||
/// Global unique identifier of this item's library section.
|
||||
String? get libraryGlobalKey => serverId != null && libraryId != null ? buildGlobalKey(serverId!, libraryId!) : null;
|
||||
|
||||
/// Parent rating keys for hierarchical invalidation. For an episode:
|
||||
/// `[seasonId, showId]`. For a season: `[showId]`. For a movie: `[]`.
|
||||
List<String> get parentChain => [?parentId, ?grandparentId];
|
||||
|
||||
/// Whether this item has started but not finished playback.
|
||||
bool get hasActiveProgress {
|
||||
if (durationMs == null || viewOffsetMs == null) return false;
|
||||
return viewOffsetMs! > 0 && viewOffsetMs! < durationMs!;
|
||||
}
|
||||
|
||||
/// Whether this container (show/season) has some but not all leaves watched.
|
||||
bool get isPartiallyWatched =>
|
||||
viewedLeafCount != null && leafCount != null && viewedLeafCount! > 0 && viewedLeafCount! < leafCount!;
|
||||
|
||||
/// Whether the item is fully watched. Series/seasons consult leaf counts;
|
||||
/// individual movies/episodes use [viewCount].
|
||||
bool get isWatched {
|
||||
if (leafCount != null && viewedLeafCount != null) {
|
||||
return viewedLeafCount! >= leafCount!;
|
||||
}
|
||||
return viewCount != null && viewCount! > 0;
|
||||
}
|
||||
|
||||
/// Display-friendly title that prefers the show name for episodes/seasons.
|
||||
String get displayTitle {
|
||||
if ((kind == MediaKind.episode || kind == MediaKind.season) && grandparentTitle != null) {
|
||||
return grandparentTitle!;
|
||||
}
|
||||
if (kind == MediaKind.season && parentTitle != null) {
|
||||
return parentTitle!;
|
||||
}
|
||||
return title ?? '';
|
||||
}
|
||||
|
||||
/// Subtitle line shown below [displayTitle] for episodes/seasons.
|
||||
String? get displaySubtitle {
|
||||
if (kind == MediaKind.episode || kind == MediaKind.season) {
|
||||
if (grandparentTitle != null || (kind == MediaKind.season && parentTitle != null)) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Plex-only edition label (e.g. "Director's Cut"). Returns null on
|
||||
/// backends that don't model editions; lets callers avoid type-narrowing
|
||||
/// to [PlexMediaItem] just to read this field.
|
||||
String? get editionTitle => null;
|
||||
|
||||
/// Returns the appropriate poster path based on episode poster mode.
|
||||
///
|
||||
/// For episodes:
|
||||
/// - `seriesPoster`: grandparentThumb (series poster)
|
||||
/// - `seasonPoster`: parentThumb (season poster)
|
||||
/// - `episodeThumbnail`: thumb (16:9 episode still)
|
||||
///
|
||||
/// For seasons: returns grandparentThumb (series poster), or art/thumb in
|
||||
/// mixed hub context.
|
||||
/// For movies/shows in mixed hub context with episode-thumbnail mode:
|
||||
/// returns art (16:9 background).
|
||||
/// For other types: returns thumb.
|
||||
String? posterThumb({EpisodePosterMode mode = EpisodePosterMode.seriesPoster, bool mixedHubContext = false}) {
|
||||
if (kind == MediaKind.episode) {
|
||||
switch (mode) {
|
||||
case EpisodePosterMode.episodeThumbnail:
|
||||
return thumbPath;
|
||||
case EpisodePosterMode.seasonPoster:
|
||||
return parentThumbPath ?? grandparentThumbPath ?? thumbPath;
|
||||
case EpisodePosterMode.seriesPoster:
|
||||
return grandparentThumbPath ?? thumbPath;
|
||||
}
|
||||
} else if (kind == MediaKind.season) {
|
||||
if (mixedHubContext && mode == EpisodePosterMode.episodeThumbnail) {
|
||||
return artPath ?? thumbPath;
|
||||
}
|
||||
if (grandparentThumbPath != null) {
|
||||
return grandparentThumbPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (mixedHubContext &&
|
||||
mode == EpisodePosterMode.episodeThumbnail &&
|
||||
(kind == MediaKind.movie || kind == MediaKind.show)) {
|
||||
return artPath ?? thumbPath;
|
||||
}
|
||||
|
||||
return thumbPath;
|
||||
}
|
||||
|
||||
/// Secondary poster path to try when [posterThumb] returns an image URL that
|
||||
/// exists syntactically but the server cannot serve it.
|
||||
String? posterThumbFallback({EpisodePosterMode mode = EpisodePosterMode.seriesPoster, bool mixedHubContext = false}) {
|
||||
if (kind != MediaKind.episode || mode != EpisodePosterMode.seasonPoster) return null;
|
||||
final fallback = grandparentThumbPath ?? thumbPath;
|
||||
return fallback != null && fallback != posterThumb(mode: mode, mixedHubContext: mixedHubContext) ? fallback : null;
|
||||
}
|
||||
|
||||
/// True when the item should render in 16:9.
|
||||
/// - Clips are always 16:9.
|
||||
/// - Episodes are 16:9 in `episodeThumbnail` mode.
|
||||
/// - Movies/shows/seasons are 16:9 in mixed-hub `episodeThumbnail` context.
|
||||
bool usesWideAspectRatio(EpisodePosterMode mode, {bool mixedHubContext = false}) {
|
||||
if (kind == MediaKind.clip) return true;
|
||||
if (kind == MediaKind.episode && mode == EpisodePosterMode.episodeThumbnail) {
|
||||
return true;
|
||||
}
|
||||
if (mixedHubContext &&
|
||||
mode == EpisodePosterMode.episodeThumbnail &&
|
||||
(kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.season)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns the best hero art path based on the container's aspect ratio.
|
||||
/// Uses backgroundSquare when the container is closer to 1:1 than 16:9.
|
||||
String? heroArt({required double containerAspectRatio}) {
|
||||
final candidates = heroArtCandidates(containerAspectRatio: containerAspectRatio);
|
||||
if (candidates.isEmpty) return null;
|
||||
return candidates.first;
|
||||
}
|
||||
|
||||
/// Returns hero art candidates in display-preference order.
|
||||
/// Near-square containers prefer square art, then fall back to wide cover art.
|
||||
List<String> heroArtCandidates({required double containerAspectRatio}) {
|
||||
// Threshold = midpoint of 1:1 (1.0) and 16:9 (~1.78) ≈ 1.39
|
||||
final preferred = containerAspectRatio < 1.39 ? [backgroundSquarePath, artPath] : [artPath, backgroundSquarePath];
|
||||
|
||||
final candidates = <String>[];
|
||||
for (final path in preferred) {
|
||||
if (path == null || path.isEmpty || candidates.contains(path)) continue;
|
||||
candidates.add(path);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
MediaItem copyWith({
|
||||
String? id,
|
||||
MediaBackend? backend,
|
||||
MediaKind? kind,
|
||||
String? guid,
|
||||
String? title,
|
||||
String? titleSort,
|
||||
String? summary,
|
||||
String? tagline,
|
||||
String? originalTitle,
|
||||
String? studio,
|
||||
int? year,
|
||||
String? originallyAvailableAt,
|
||||
String? contentRating,
|
||||
String? parentId,
|
||||
String? parentTitle,
|
||||
String? parentThumbPath,
|
||||
int? parentIndex,
|
||||
int? index,
|
||||
String? grandparentId,
|
||||
String? grandparentTitle,
|
||||
String? grandparentThumbPath,
|
||||
String? grandparentArtPath,
|
||||
String? thumbPath,
|
||||
String? artPath,
|
||||
String? clearLogoPath,
|
||||
String? backgroundSquarePath,
|
||||
int? durationMs,
|
||||
int? viewOffsetMs,
|
||||
int? viewCount,
|
||||
int? lastViewedAt,
|
||||
int? leafCount,
|
||||
int? viewedLeafCount,
|
||||
int? childCount,
|
||||
int? addedAt,
|
||||
int? updatedAt,
|
||||
double? rating,
|
||||
double? userRating,
|
||||
List<String>? genres,
|
||||
List<String>? directors,
|
||||
List<String>? writers,
|
||||
List<String>? producers,
|
||||
List<String>? countries,
|
||||
List<String>? collections,
|
||||
List<String>? labels,
|
||||
List<String>? styles,
|
||||
List<String>? moods,
|
||||
List<MediaRole>? roles,
|
||||
List<MediaVersion>? mediaVersions,
|
||||
String? libraryId,
|
||||
String? libraryTitle,
|
||||
String? audioLanguage,
|
||||
|
||||
/// Plex-only — forwarded only when this item is a [PlexMediaItem].
|
||||
String? subtitleLanguage,
|
||||
int? subtitleMode,
|
||||
String? serverId,
|
||||
String? serverName,
|
||||
Map<String, Object?>? raw,
|
||||
}) {
|
||||
return MediaItem(
|
||||
id: id ?? this.id,
|
||||
backend: backend ?? this.backend,
|
||||
kind: kind ?? this.kind,
|
||||
guid: guid ?? this.guid,
|
||||
title: title ?? this.title,
|
||||
titleSort: titleSort ?? this.titleSort,
|
||||
summary: summary ?? this.summary,
|
||||
tagline: tagline ?? this.tagline,
|
||||
originalTitle: originalTitle ?? this.originalTitle,
|
||||
studio: studio ?? this.studio,
|
||||
year: year ?? this.year,
|
||||
originallyAvailableAt: originallyAvailableAt ?? this.originallyAvailableAt,
|
||||
contentRating: contentRating ?? this.contentRating,
|
||||
parentId: parentId ?? this.parentId,
|
||||
parentTitle: parentTitle ?? this.parentTitle,
|
||||
parentThumbPath: parentThumbPath ?? this.parentThumbPath,
|
||||
parentIndex: parentIndex ?? this.parentIndex,
|
||||
index: index ?? this.index,
|
||||
grandparentId: grandparentId ?? this.grandparentId,
|
||||
grandparentTitle: grandparentTitle ?? this.grandparentTitle,
|
||||
grandparentThumbPath: grandparentThumbPath ?? this.grandparentThumbPath,
|
||||
grandparentArtPath: grandparentArtPath ?? this.grandparentArtPath,
|
||||
thumbPath: thumbPath ?? this.thumbPath,
|
||||
artPath: artPath ?? this.artPath,
|
||||
clearLogoPath: clearLogoPath ?? this.clearLogoPath,
|
||||
backgroundSquarePath: backgroundSquarePath ?? this.backgroundSquarePath,
|
||||
durationMs: durationMs ?? this.durationMs,
|
||||
viewOffsetMs: viewOffsetMs ?? this.viewOffsetMs,
|
||||
viewCount: viewCount ?? this.viewCount,
|
||||
lastViewedAt: lastViewedAt ?? this.lastViewedAt,
|
||||
leafCount: leafCount ?? this.leafCount,
|
||||
viewedLeafCount: viewedLeafCount ?? this.viewedLeafCount,
|
||||
childCount: childCount ?? this.childCount,
|
||||
addedAt: addedAt ?? this.addedAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
rating: rating ?? this.rating,
|
||||
userRating: userRating ?? this.userRating,
|
||||
genres: genres ?? this.genres,
|
||||
directors: directors ?? this.directors,
|
||||
writers: writers ?? this.writers,
|
||||
producers: producers ?? this.producers,
|
||||
countries: countries ?? this.countries,
|
||||
collections: collections ?? this.collections,
|
||||
labels: labels ?? this.labels,
|
||||
styles: styles ?? this.styles,
|
||||
moods: moods ?? this.moods,
|
||||
roles: roles ?? this.roles,
|
||||
mediaVersions: mediaVersions ?? this.mediaVersions,
|
||||
libraryId: libraryId ?? this.libraryId,
|
||||
libraryTitle: libraryTitle ?? this.libraryTitle,
|
||||
audioLanguage: audioLanguage ?? this.audioLanguage,
|
||||
// [subtitleLanguage] / [subtitleMode] are Plex-only fields. Base
|
||||
// [MediaItem] doesn't carry them; [PlexMediaItem.copyWith] overrides
|
||||
// this method and forwards its own copies. For Jellyfin items the
|
||||
// params are silently dropped.
|
||||
subtitleLanguage: subtitleLanguage,
|
||||
subtitleMode: subtitleMode,
|
||||
serverId: serverId ?? this.serverId,
|
||||
serverName: serverName ?? this.serverName,
|
||||
raw: raw ?? this.raw,
|
||||
);
|
||||
}
|
||||
|
||||
/// Serialize to a backend-neutral JSON map. Used by the offline cache so
|
||||
/// downloads retain their metadata without round-tripping through a
|
||||
/// backend-specific shape.
|
||||
///
|
||||
/// Subclasses extend this with their own backend-specific keys
|
||||
/// ([PlexMediaItem.toJson] adds the Plex-only fields).
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'backend': backend.id,
|
||||
'kind': kind.id,
|
||||
if (guid != null) 'guid': guid,
|
||||
if (title != null) 'title': title,
|
||||
if (titleSort != null) 'titleSort': titleSort,
|
||||
if (summary != null) 'summary': summary,
|
||||
if (tagline != null) 'tagline': tagline,
|
||||
if (originalTitle != null) 'originalTitle': originalTitle,
|
||||
if (studio != null) 'studio': studio,
|
||||
if (year != null) 'year': year,
|
||||
if (originallyAvailableAt != null) 'originallyAvailableAt': originallyAvailableAt,
|
||||
if (contentRating != null) 'contentRating': contentRating,
|
||||
if (parentId != null) 'parentId': parentId,
|
||||
if (parentTitle != null) 'parentTitle': parentTitle,
|
||||
if (parentThumbPath != null) 'parentThumbPath': parentThumbPath,
|
||||
if (parentIndex != null) 'parentIndex': parentIndex,
|
||||
if (index != null) 'index': index,
|
||||
if (grandparentId != null) 'grandparentId': grandparentId,
|
||||
if (grandparentTitle != null) 'grandparentTitle': grandparentTitle,
|
||||
if (grandparentThumbPath != null) 'grandparentThumbPath': grandparentThumbPath,
|
||||
if (grandparentArtPath != null) 'grandparentArtPath': grandparentArtPath,
|
||||
if (thumbPath != null) 'thumbPath': thumbPath,
|
||||
if (artPath != null) 'artPath': artPath,
|
||||
if (clearLogoPath != null) 'clearLogoPath': clearLogoPath,
|
||||
if (backgroundSquarePath != null) 'backgroundSquarePath': backgroundSquarePath,
|
||||
if (durationMs != null) 'durationMs': durationMs,
|
||||
if (viewOffsetMs != null) 'viewOffsetMs': viewOffsetMs,
|
||||
if (viewCount != null) 'viewCount': viewCount,
|
||||
if (lastViewedAt != null) 'lastViewedAt': lastViewedAt,
|
||||
if (leafCount != null) 'leafCount': leafCount,
|
||||
if (viewedLeafCount != null) 'viewedLeafCount': viewedLeafCount,
|
||||
if (childCount != null) 'childCount': childCount,
|
||||
if (addedAt != null) 'addedAt': addedAt,
|
||||
if (updatedAt != null) 'updatedAt': updatedAt,
|
||||
if (rating != null) 'rating': rating,
|
||||
if (userRating != null) 'userRating': userRating,
|
||||
if (genres != null) 'genres': genres,
|
||||
if (directors != null) 'directors': directors,
|
||||
if (writers != null) 'writers': writers,
|
||||
if (producers != null) 'producers': producers,
|
||||
if (countries != null) 'countries': countries,
|
||||
if (collections != null) 'collections': collections,
|
||||
if (labels != null) 'labels': labels,
|
||||
if (styles != null) 'styles': styles,
|
||||
if (moods != null) 'moods': moods,
|
||||
if (roles != null) 'roles': [for (final r in roles!) _roleToJson(r)],
|
||||
if (mediaVersions != null) 'mediaVersions': [for (final v in mediaVersions!) _versionToJson(v)],
|
||||
if (libraryId != null) 'libraryId': libraryId,
|
||||
if (libraryTitle != null) 'libraryTitle': libraryTitle,
|
||||
if (audioLanguage != null) 'audioLanguage': audioLanguage,
|
||||
if (serverId != null) 'serverId': serverId,
|
||||
if (serverName != null) 'serverName': serverName,
|
||||
if (raw != null) 'raw': raw,
|
||||
};
|
||||
}
|
||||
|
||||
/// Restore a [MediaItem] from a [toJson] payload. Dispatches to
|
||||
/// [PlexMediaItem.fromJson] when the payload's `backend` tag is Plex so
|
||||
/// the Plex-only fields round-trip correctly. Unknown shapes degrade to a
|
||||
/// minimal item carrying just `id` so cache misses don't crash.
|
||||
factory MediaItem.fromJson(Map<String, dynamic> json) {
|
||||
final backend = MediaBackend.fromString(json['backend'] as String?);
|
||||
if (backend == MediaBackend.plex) return PlexMediaItem.fromJson(json);
|
||||
return JellyfinMediaItem.fromJson(json);
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
part of '../media_item.dart';
|
||||
|
||||
/// Backend-tagged concrete subclass for items sourced from a Jellyfin
|
||||
/// server. Carries only the backend-neutral fields — Plex-only fields
|
||||
/// (trailerKey, audienceRating, etc.) live on [PlexMediaItem] instead.
|
||||
final class JellyfinMediaItem extends MediaItem {
|
||||
/// Jellyfin per-playlist item id — only set when the item came out of
|
||||
/// `/Playlists/{id}/Items`. Used as the `entryIds` / move-target id for
|
||||
/// the playlist write endpoints. Null outside playlist contexts.
|
||||
final String? playlistItemId;
|
||||
|
||||
const JellyfinMediaItem({
|
||||
required super.id,
|
||||
required super.kind,
|
||||
super.guid,
|
||||
super.title,
|
||||
super.titleSort,
|
||||
super.summary,
|
||||
super.tagline,
|
||||
super.originalTitle,
|
||||
super.studio,
|
||||
super.year,
|
||||
super.originallyAvailableAt,
|
||||
super.contentRating,
|
||||
super.parentId,
|
||||
super.parentTitle,
|
||||
super.parentThumbPath,
|
||||
super.parentIndex,
|
||||
super.index,
|
||||
super.grandparentId,
|
||||
super.grandparentTitle,
|
||||
super.grandparentThumbPath,
|
||||
super.grandparentArtPath,
|
||||
super.thumbPath,
|
||||
super.artPath,
|
||||
super.clearLogoPath,
|
||||
super.backgroundSquarePath,
|
||||
super.durationMs,
|
||||
super.viewOffsetMs,
|
||||
super.viewCount,
|
||||
super.lastViewedAt,
|
||||
super.leafCount,
|
||||
super.viewedLeafCount,
|
||||
super.childCount,
|
||||
super.addedAt,
|
||||
super.updatedAt,
|
||||
super.rating,
|
||||
super.userRating,
|
||||
super.genres,
|
||||
super.directors,
|
||||
super.writers,
|
||||
super.producers,
|
||||
super.countries,
|
||||
super.collections,
|
||||
super.labels,
|
||||
super.styles,
|
||||
super.moods,
|
||||
super.roles,
|
||||
super.mediaVersions,
|
||||
super.libraryId,
|
||||
super.libraryTitle,
|
||||
super.audioLanguage,
|
||||
this.playlistItemId,
|
||||
super.serverId,
|
||||
super.serverName,
|
||||
super.raw,
|
||||
}) : super._(backend: MediaBackend.jellyfin);
|
||||
|
||||
/// Override the base [MediaItem.copyWith] so [playlistItemId] survives
|
||||
/// round-trips through the absolutizer (which calls copyWith to rewrite
|
||||
/// image paths). Without this, every Jellyfin playlist item came out with
|
||||
/// `playlistItemId == null` after mapping, making the move/remove endpoints
|
||||
/// silently no-op.
|
||||
@override
|
||||
JellyfinMediaItem copyWith({
|
||||
String? id,
|
||||
MediaBackend? backend,
|
||||
MediaKind? kind,
|
||||
String? guid,
|
||||
String? title,
|
||||
String? titleSort,
|
||||
String? summary,
|
||||
String? tagline,
|
||||
String? originalTitle,
|
||||
String? studio,
|
||||
int? year,
|
||||
String? originallyAvailableAt,
|
||||
String? contentRating,
|
||||
String? parentId,
|
||||
String? parentTitle,
|
||||
String? parentThumbPath,
|
||||
int? parentIndex,
|
||||
int? index,
|
||||
String? grandparentId,
|
||||
String? grandparentTitle,
|
||||
String? grandparentThumbPath,
|
||||
String? grandparentArtPath,
|
||||
String? thumbPath,
|
||||
String? artPath,
|
||||
String? clearLogoPath,
|
||||
String? backgroundSquarePath,
|
||||
int? durationMs,
|
||||
int? viewOffsetMs,
|
||||
int? viewCount,
|
||||
int? lastViewedAt,
|
||||
int? leafCount,
|
||||
int? viewedLeafCount,
|
||||
int? childCount,
|
||||
int? addedAt,
|
||||
int? updatedAt,
|
||||
double? rating,
|
||||
double? userRating,
|
||||
List<String>? genres,
|
||||
List<String>? directors,
|
||||
List<String>? writers,
|
||||
List<String>? producers,
|
||||
List<String>? countries,
|
||||
List<String>? collections,
|
||||
List<String>? labels,
|
||||
List<String>? styles,
|
||||
List<String>? moods,
|
||||
List<MediaRole>? roles,
|
||||
List<MediaVersion>? mediaVersions,
|
||||
String? libraryId,
|
||||
String? libraryTitle,
|
||||
String? audioLanguage,
|
||||
String? subtitleLanguage,
|
||||
int? subtitleMode,
|
||||
String? playlistItemId,
|
||||
String? serverId,
|
||||
String? serverName,
|
||||
Map<String, Object?>? raw,
|
||||
}) {
|
||||
return JellyfinMediaItem(
|
||||
id: id ?? this.id,
|
||||
kind: kind ?? this.kind,
|
||||
guid: guid ?? this.guid,
|
||||
title: title ?? this.title,
|
||||
titleSort: titleSort ?? this.titleSort,
|
||||
summary: summary ?? this.summary,
|
||||
tagline: tagline ?? this.tagline,
|
||||
originalTitle: originalTitle ?? this.originalTitle,
|
||||
studio: studio ?? this.studio,
|
||||
year: year ?? this.year,
|
||||
originallyAvailableAt: originallyAvailableAt ?? this.originallyAvailableAt,
|
||||
contentRating: contentRating ?? this.contentRating,
|
||||
parentId: parentId ?? this.parentId,
|
||||
parentTitle: parentTitle ?? this.parentTitle,
|
||||
parentThumbPath: parentThumbPath ?? this.parentThumbPath,
|
||||
parentIndex: parentIndex ?? this.parentIndex,
|
||||
index: index ?? this.index,
|
||||
grandparentId: grandparentId ?? this.grandparentId,
|
||||
grandparentTitle: grandparentTitle ?? this.grandparentTitle,
|
||||
grandparentThumbPath: grandparentThumbPath ?? this.grandparentThumbPath,
|
||||
grandparentArtPath: grandparentArtPath ?? this.grandparentArtPath,
|
||||
thumbPath: thumbPath ?? this.thumbPath,
|
||||
artPath: artPath ?? this.artPath,
|
||||
clearLogoPath: clearLogoPath ?? this.clearLogoPath,
|
||||
backgroundSquarePath: backgroundSquarePath ?? this.backgroundSquarePath,
|
||||
durationMs: durationMs ?? this.durationMs,
|
||||
viewOffsetMs: viewOffsetMs ?? this.viewOffsetMs,
|
||||
viewCount: viewCount ?? this.viewCount,
|
||||
lastViewedAt: lastViewedAt ?? this.lastViewedAt,
|
||||
leafCount: leafCount ?? this.leafCount,
|
||||
viewedLeafCount: viewedLeafCount ?? this.viewedLeafCount,
|
||||
childCount: childCount ?? this.childCount,
|
||||
addedAt: addedAt ?? this.addedAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
rating: rating ?? this.rating,
|
||||
userRating: userRating ?? this.userRating,
|
||||
genres: genres ?? this.genres,
|
||||
directors: directors ?? this.directors,
|
||||
writers: writers ?? this.writers,
|
||||
producers: producers ?? this.producers,
|
||||
countries: countries ?? this.countries,
|
||||
collections: collections ?? this.collections,
|
||||
labels: labels ?? this.labels,
|
||||
styles: styles ?? this.styles,
|
||||
moods: moods ?? this.moods,
|
||||
roles: roles ?? this.roles,
|
||||
mediaVersions: mediaVersions ?? this.mediaVersions,
|
||||
libraryId: libraryId ?? this.libraryId,
|
||||
libraryTitle: libraryTitle ?? this.libraryTitle,
|
||||
audioLanguage: audioLanguage ?? this.audioLanguage,
|
||||
playlistItemId: playlistItemId ?? this.playlistItemId,
|
||||
serverId: serverId ?? this.serverId,
|
||||
serverName: serverName ?? this.serverName,
|
||||
raw: raw ?? this.raw,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {...super.toJson(), if (playlistItemId != null) 'playlistItemId': playlistItemId};
|
||||
}
|
||||
|
||||
/// Restore a [JellyfinMediaItem] from a [toJson] payload. Used as the
|
||||
/// non-Plex fallback by [MediaItem.fromJson].
|
||||
factory JellyfinMediaItem.fromJson(Map<String, dynamic> json) {
|
||||
final base = _parseBaseFields(json);
|
||||
return JellyfinMediaItem(
|
||||
id: base.id,
|
||||
kind: base.kind,
|
||||
guid: base.guid,
|
||||
title: base.title,
|
||||
titleSort: base.titleSort,
|
||||
summary: base.summary,
|
||||
tagline: base.tagline,
|
||||
originalTitle: base.originalTitle,
|
||||
studio: base.studio,
|
||||
year: base.year,
|
||||
originallyAvailableAt: base.originallyAvailableAt,
|
||||
contentRating: base.contentRating,
|
||||
parentId: base.parentId,
|
||||
parentTitle: base.parentTitle,
|
||||
parentThumbPath: base.parentThumbPath,
|
||||
parentIndex: base.parentIndex,
|
||||
index: base.index,
|
||||
grandparentId: base.grandparentId,
|
||||
grandparentTitle: base.grandparentTitle,
|
||||
grandparentThumbPath: base.grandparentThumbPath,
|
||||
grandparentArtPath: base.grandparentArtPath,
|
||||
thumbPath: base.thumbPath,
|
||||
artPath: base.artPath,
|
||||
clearLogoPath: base.clearLogoPath,
|
||||
backgroundSquarePath: base.backgroundSquarePath,
|
||||
durationMs: base.durationMs,
|
||||
viewOffsetMs: base.viewOffsetMs,
|
||||
viewCount: base.viewCount,
|
||||
lastViewedAt: base.lastViewedAt,
|
||||
leafCount: base.leafCount,
|
||||
viewedLeafCount: base.viewedLeafCount,
|
||||
childCount: base.childCount,
|
||||
addedAt: base.addedAt,
|
||||
updatedAt: base.updatedAt,
|
||||
rating: base.rating,
|
||||
userRating: base.userRating,
|
||||
genres: base.genres,
|
||||
directors: base.directors,
|
||||
writers: base.writers,
|
||||
producers: base.producers,
|
||||
countries: base.countries,
|
||||
collections: base.collections,
|
||||
labels: base.labels,
|
||||
styles: base.styles,
|
||||
moods: base.moods,
|
||||
roles: base.roles,
|
||||
mediaVersions: base.mediaVersions,
|
||||
libraryId: base.libraryId,
|
||||
libraryTitle: base.libraryTitle,
|
||||
audioLanguage: base.audioLanguage,
|
||||
playlistItemId: json['playlistItemId'] as String?,
|
||||
serverId: base.serverId,
|
||||
serverName: base.serverName,
|
||||
raw: base.raw,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
part of '../media_item.dart';
|
||||
|
||||
/// Shared parsing of the backend-neutral fields. Returns a typed record
|
||||
/// consumed by both [JellyfinMediaItem.fromJson] and
|
||||
/// [PlexMediaItem.fromJson] (which layers the Plex-only fields on top).
|
||||
typedef _BaseFields = ({
|
||||
String id,
|
||||
MediaKind kind,
|
||||
String? guid,
|
||||
String? title,
|
||||
String? titleSort,
|
||||
String? summary,
|
||||
String? tagline,
|
||||
String? originalTitle,
|
||||
String? studio,
|
||||
int? year,
|
||||
String? originallyAvailableAt,
|
||||
String? contentRating,
|
||||
String? parentId,
|
||||
String? parentTitle,
|
||||
String? parentThumbPath,
|
||||
int? parentIndex,
|
||||
int? index,
|
||||
String? grandparentId,
|
||||
String? grandparentTitle,
|
||||
String? grandparentThumbPath,
|
||||
String? grandparentArtPath,
|
||||
String? thumbPath,
|
||||
String? artPath,
|
||||
String? clearLogoPath,
|
||||
String? backgroundSquarePath,
|
||||
int? durationMs,
|
||||
int? viewOffsetMs,
|
||||
int? viewCount,
|
||||
int? lastViewedAt,
|
||||
int? leafCount,
|
||||
int? viewedLeafCount,
|
||||
int? childCount,
|
||||
int? addedAt,
|
||||
int? updatedAt,
|
||||
double? rating,
|
||||
double? userRating,
|
||||
List<String>? genres,
|
||||
List<String>? directors,
|
||||
List<String>? writers,
|
||||
List<String>? producers,
|
||||
List<String>? countries,
|
||||
List<String>? collections,
|
||||
List<String>? labels,
|
||||
List<String>? styles,
|
||||
List<String>? moods,
|
||||
List<MediaRole>? roles,
|
||||
List<MediaVersion>? mediaVersions,
|
||||
String? libraryId,
|
||||
String? libraryTitle,
|
||||
String? audioLanguage,
|
||||
String? serverId,
|
||||
String? serverName,
|
||||
Map<String, Object?>? raw,
|
||||
});
|
||||
|
||||
_BaseFields _parseBaseFields(Map<String, dynamic> json) {
|
||||
final rolesRaw = json['roles'];
|
||||
final versionsRaw = json['mediaVersions'];
|
||||
return (
|
||||
id: (json['id'] ?? '').toString(),
|
||||
kind: MediaKind.fromString(json['kind'] as String?),
|
||||
guid: json['guid'] as String?,
|
||||
title: json['title'] as String?,
|
||||
titleSort: json['titleSort'] as String?,
|
||||
summary: json['summary'] as String?,
|
||||
tagline: json['tagline'] as String?,
|
||||
originalTitle: json['originalTitle'] as String?,
|
||||
studio: json['studio'] as String?,
|
||||
year: flexibleInt(json['year']),
|
||||
originallyAvailableAt: json['originallyAvailableAt'] as String?,
|
||||
contentRating: json['contentRating'] as String?,
|
||||
parentId: json['parentId'] as String?,
|
||||
parentTitle: json['parentTitle'] as String?,
|
||||
parentThumbPath: json['parentThumbPath'] as String?,
|
||||
parentIndex: flexibleInt(json['parentIndex']),
|
||||
index: flexibleInt(json['index']),
|
||||
grandparentId: json['grandparentId'] as String?,
|
||||
grandparentTitle: json['grandparentTitle'] as String?,
|
||||
grandparentThumbPath: json['grandparentThumbPath'] as String?,
|
||||
grandparentArtPath: json['grandparentArtPath'] as String?,
|
||||
thumbPath: json['thumbPath'] as String?,
|
||||
artPath: json['artPath'] as String?,
|
||||
clearLogoPath: json['clearLogoPath'] as String?,
|
||||
backgroundSquarePath: json['backgroundSquarePath'] as String?,
|
||||
durationMs: flexibleInt(json['durationMs']),
|
||||
viewOffsetMs: flexibleInt(json['viewOffsetMs']),
|
||||
viewCount: flexibleInt(json['viewCount']),
|
||||
lastViewedAt: flexibleInt(json['lastViewedAt']),
|
||||
leafCount: flexibleInt(json['leafCount']),
|
||||
viewedLeafCount: flexibleInt(json['viewedLeafCount']),
|
||||
childCount: flexibleInt(json['childCount']),
|
||||
addedAt: flexibleInt(json['addedAt']),
|
||||
updatedAt: flexibleInt(json['updatedAt']),
|
||||
rating: flexibleDouble(json['rating']),
|
||||
userRating: flexibleDouble(json['userRating']),
|
||||
genres: _stringList(json['genres']),
|
||||
directors: _stringList(json['directors']),
|
||||
writers: _stringList(json['writers']),
|
||||
producers: _stringList(json['producers']),
|
||||
countries: _stringList(json['countries']),
|
||||
collections: _stringList(json['collections']),
|
||||
labels: _stringList(json['labels']),
|
||||
styles: _stringList(json['styles']),
|
||||
moods: _stringList(json['moods']),
|
||||
roles: rolesRaw is List
|
||||
? [
|
||||
for (final r in rolesRaw)
|
||||
if (r is Map<String, dynamic>) _roleFromJson(r),
|
||||
]
|
||||
: null,
|
||||
mediaVersions: versionsRaw is List
|
||||
? [
|
||||
for (final v in versionsRaw)
|
||||
if (v is Map<String, dynamic>) _versionFromJson(v),
|
||||
]
|
||||
: null,
|
||||
libraryId: json['libraryId'] as String?,
|
||||
libraryTitle: json['libraryTitle'] as String?,
|
||||
audioLanguage: json['audioLanguage'] as String?,
|
||||
serverId: json['serverId'] as String?,
|
||||
serverName: json['serverName'] as String?,
|
||||
raw: json['raw'] is Map ? Map<String, Object?>.from(json['raw'] as Map) : null,
|
||||
);
|
||||
}
|
||||
|
||||
List<String>? _stringList(Object? raw) {
|
||||
return stringListFromRaw(raw, stringify: true);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _roleToJson(MediaRole role) => {
|
||||
if (role.id != null) 'id': role.id,
|
||||
'tag': role.tag,
|
||||
if (role.role != null) 'role': role.role,
|
||||
if (role.thumbPath != null) 'thumbPath': role.thumbPath,
|
||||
};
|
||||
|
||||
MediaRole _roleFromJson(Map<String, dynamic> json) => MediaRole(
|
||||
id: json['id'] as String?,
|
||||
tag: (json['tag'] ?? '').toString(),
|
||||
role: json['role'] as String?,
|
||||
thumbPath: json['thumbPath'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _versionToJson(MediaVersion v) => {
|
||||
'id': v.id,
|
||||
if (v.width != null) 'width': v.width,
|
||||
if (v.height != null) 'height': v.height,
|
||||
if (v.videoResolution != null) 'videoResolution': v.videoResolution,
|
||||
if (v.videoCodec != null) 'videoCodec': v.videoCodec,
|
||||
if (v.bitrate != null) 'bitrate': v.bitrate,
|
||||
if (v.container != null) 'container': v.container,
|
||||
if (v.name != null) 'name': v.name,
|
||||
'parts': [
|
||||
for (final p in v.parts)
|
||||
{
|
||||
'id': p.id,
|
||||
if (p.streamPath != null) 'streamPath': p.streamPath,
|
||||
if (p.sizeBytes != null) 'sizeBytes': p.sizeBytes,
|
||||
if (p.container != null) 'container': p.container,
|
||||
if (p.durationMs != null) 'durationMs': p.durationMs,
|
||||
if (p.accessible != null) 'accessible': p.accessible,
|
||||
if (p.exists != null) 'exists': p.exists,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
MediaVersion _versionFromJson(Map<String, dynamic> json) {
|
||||
final partsRaw = json['parts'];
|
||||
return MediaVersion(
|
||||
id: (json['id'] ?? '').toString(),
|
||||
width: flexibleInt(json['width']),
|
||||
height: flexibleInt(json['height']),
|
||||
videoResolution: json['videoResolution'] as String?,
|
||||
videoCodec: json['videoCodec'] as String?,
|
||||
bitrate: flexibleInt(json['bitrate']),
|
||||
container: json['container'] as String?,
|
||||
name: json['name'] as String?,
|
||||
parts: partsRaw is List
|
||||
? [
|
||||
for (final p in partsRaw)
|
||||
if (p is Map<String, dynamic>)
|
||||
MediaPart(
|
||||
id: (p['id'] ?? '').toString(),
|
||||
streamPath: p['streamPath'] as String?,
|
||||
sizeBytes: flexibleInt(p['sizeBytes']),
|
||||
container: p['container'] as String?,
|
||||
durationMs: flexibleInt(p['durationMs']),
|
||||
accessible: p['accessible'] as bool?,
|
||||
exists: p['exists'] as bool?,
|
||||
),
|
||||
]
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
part of '../media_item.dart';
|
||||
|
||||
/// Backend-tagged concrete subclass for items sourced from a Plex server.
|
||||
/// Carries the Plex-only fields that have no Jellyfin equivalent
|
||||
/// (trailerKey, playlistItemId, playQueueItemId, subtype, extraType,
|
||||
/// ratingImage, audienceRating, audienceRatingImage, editionTitle).
|
||||
/// Read sites that need these fields type-narrow with
|
||||
/// `case PlexMediaItem(:final trailerKey?)` or
|
||||
/// `if (item is PlexMediaItem) item.trailerKey`.
|
||||
final class PlexMediaItem extends MediaItem {
|
||||
/// Plex `editionTitle` — secondary title that distinguishes editions of
|
||||
/// the same movie ("Director's Cut", "Theatrical"). Jellyfin has no
|
||||
/// equivalent metadata field today.
|
||||
@override
|
||||
final String? editionTitle;
|
||||
|
||||
/// Plex `audienceRating` (e.g. Rotten Tomatoes audience score). Jellyfin's
|
||||
/// `CommunityRating` lives on [rating]; there's no separate audience field.
|
||||
final double? audienceRating;
|
||||
|
||||
/// Plex `ratingImage` URI ("rottentomatoes://image.rating.ripe"). Used by
|
||||
/// the rating chip to pick an icon. Jellyfin doesn't expose
|
||||
/// rating-source attribution.
|
||||
final String? ratingImage;
|
||||
|
||||
/// Plex `audienceRatingImage` URI — companion to [ratingImage] for the
|
||||
/// audience score icon.
|
||||
final String? audienceRatingImage;
|
||||
|
||||
/// Plex per-item subtitle language preference — persisted server-side via
|
||||
/// [PlexClient.setMetadataPreferences]. Jellyfin has no equivalent
|
||||
/// per-item write endpoint, so the field lives here rather than on the
|
||||
/// neutral [MediaItem] base.
|
||||
final String? subtitleLanguage;
|
||||
|
||||
/// Plex per-item subtitle mode (`0` = manual, `1` = always on, `2` = match
|
||||
/// audio). Jellyfin doesn't expose a comparable knob.
|
||||
final int? subtitleMode;
|
||||
|
||||
/// Plex `primaryExtraKey` — points at the main trailer extra. Jellyfin
|
||||
/// stores trailers separately via `RemoteTrailers`; not yet wired.
|
||||
final String? trailerKey;
|
||||
|
||||
/// Plex playlist item id — only set when the item came out of a
|
||||
/// server-side playlist. Jellyfin has no per-playlist-item id.
|
||||
final int? playlistItemId;
|
||||
|
||||
/// Plex play-queue item id — set when the item is part of a server-side
|
||||
/// `PlayQueue`. Jellyfin uses client-side queues; [PlaybackStateProvider]
|
||||
/// tracks synthetic IDs in a parallel map for those.
|
||||
final int? playQueueItemId;
|
||||
|
||||
/// Plex clip subtype: `trailer`, `behindTheScenes`, `deleted`, etc.
|
||||
final String? subtype;
|
||||
|
||||
/// Plex numeric extra type identifier.
|
||||
final int? extraType;
|
||||
|
||||
const PlexMediaItem({
|
||||
required super.id,
|
||||
required super.kind,
|
||||
super.guid,
|
||||
super.title,
|
||||
super.titleSort,
|
||||
super.summary,
|
||||
super.tagline,
|
||||
super.originalTitle,
|
||||
this.editionTitle,
|
||||
super.studio,
|
||||
super.year,
|
||||
super.originallyAvailableAt,
|
||||
super.contentRating,
|
||||
super.parentId,
|
||||
super.parentTitle,
|
||||
super.parentThumbPath,
|
||||
super.parentIndex,
|
||||
super.index,
|
||||
super.grandparentId,
|
||||
super.grandparentTitle,
|
||||
super.grandparentThumbPath,
|
||||
super.grandparentArtPath,
|
||||
super.thumbPath,
|
||||
super.artPath,
|
||||
super.clearLogoPath,
|
||||
super.backgroundSquarePath,
|
||||
super.durationMs,
|
||||
super.viewOffsetMs,
|
||||
super.viewCount,
|
||||
super.lastViewedAt,
|
||||
super.leafCount,
|
||||
super.viewedLeafCount,
|
||||
super.childCount,
|
||||
super.addedAt,
|
||||
super.updatedAt,
|
||||
super.rating,
|
||||
this.audienceRating,
|
||||
super.userRating,
|
||||
this.ratingImage,
|
||||
this.audienceRatingImage,
|
||||
super.genres,
|
||||
super.directors,
|
||||
super.writers,
|
||||
super.producers,
|
||||
super.countries,
|
||||
super.collections,
|
||||
super.labels,
|
||||
super.styles,
|
||||
super.moods,
|
||||
super.roles,
|
||||
super.mediaVersions,
|
||||
super.libraryId,
|
||||
super.libraryTitle,
|
||||
super.audioLanguage,
|
||||
this.subtitleLanguage,
|
||||
this.subtitleMode,
|
||||
this.trailerKey,
|
||||
this.playlistItemId,
|
||||
this.playQueueItemId,
|
||||
this.subtype,
|
||||
this.extraType,
|
||||
super.serverId,
|
||||
super.serverName,
|
||||
super.raw,
|
||||
}) : super._(backend: MediaBackend.plex);
|
||||
|
||||
@override
|
||||
PlexMediaItem copyWith({
|
||||
String? id,
|
||||
MediaBackend? backend,
|
||||
MediaKind? kind,
|
||||
String? guid,
|
||||
String? title,
|
||||
String? titleSort,
|
||||
String? summary,
|
||||
String? tagline,
|
||||
String? originalTitle,
|
||||
String? editionTitle,
|
||||
String? studio,
|
||||
int? year,
|
||||
String? originallyAvailableAt,
|
||||
String? contentRating,
|
||||
String? parentId,
|
||||
String? parentTitle,
|
||||
String? parentThumbPath,
|
||||
int? parentIndex,
|
||||
int? index,
|
||||
String? grandparentId,
|
||||
String? grandparentTitle,
|
||||
String? grandparentThumbPath,
|
||||
String? grandparentArtPath,
|
||||
String? thumbPath,
|
||||
String? artPath,
|
||||
String? clearLogoPath,
|
||||
String? backgroundSquarePath,
|
||||
int? durationMs,
|
||||
int? viewOffsetMs,
|
||||
int? viewCount,
|
||||
int? lastViewedAt,
|
||||
int? leafCount,
|
||||
int? viewedLeafCount,
|
||||
int? childCount,
|
||||
int? addedAt,
|
||||
int? updatedAt,
|
||||
double? rating,
|
||||
double? audienceRating,
|
||||
double? userRating,
|
||||
String? ratingImage,
|
||||
String? audienceRatingImage,
|
||||
List<String>? genres,
|
||||
List<String>? directors,
|
||||
List<String>? writers,
|
||||
List<String>? producers,
|
||||
List<String>? countries,
|
||||
List<String>? collections,
|
||||
List<String>? labels,
|
||||
List<String>? styles,
|
||||
List<String>? moods,
|
||||
List<MediaRole>? roles,
|
||||
List<MediaVersion>? mediaVersions,
|
||||
String? libraryId,
|
||||
String? libraryTitle,
|
||||
String? audioLanguage,
|
||||
String? subtitleLanguage,
|
||||
int? subtitleMode,
|
||||
String? trailerKey,
|
||||
int? playlistItemId,
|
||||
int? playQueueItemId,
|
||||
String? subtype,
|
||||
int? extraType,
|
||||
String? serverId,
|
||||
String? serverName,
|
||||
Map<String, Object?>? raw,
|
||||
}) {
|
||||
return PlexMediaItem(
|
||||
id: id ?? this.id,
|
||||
kind: kind ?? this.kind,
|
||||
guid: guid ?? this.guid,
|
||||
title: title ?? this.title,
|
||||
titleSort: titleSort ?? this.titleSort,
|
||||
summary: summary ?? this.summary,
|
||||
tagline: tagline ?? this.tagline,
|
||||
originalTitle: originalTitle ?? this.originalTitle,
|
||||
editionTitle: editionTitle ?? this.editionTitle,
|
||||
studio: studio ?? this.studio,
|
||||
year: year ?? this.year,
|
||||
originallyAvailableAt: originallyAvailableAt ?? this.originallyAvailableAt,
|
||||
contentRating: contentRating ?? this.contentRating,
|
||||
parentId: parentId ?? this.parentId,
|
||||
parentTitle: parentTitle ?? this.parentTitle,
|
||||
parentThumbPath: parentThumbPath ?? this.parentThumbPath,
|
||||
parentIndex: parentIndex ?? this.parentIndex,
|
||||
index: index ?? this.index,
|
||||
grandparentId: grandparentId ?? this.grandparentId,
|
||||
grandparentTitle: grandparentTitle ?? this.grandparentTitle,
|
||||
grandparentThumbPath: grandparentThumbPath ?? this.grandparentThumbPath,
|
||||
grandparentArtPath: grandparentArtPath ?? this.grandparentArtPath,
|
||||
thumbPath: thumbPath ?? this.thumbPath,
|
||||
artPath: artPath ?? this.artPath,
|
||||
clearLogoPath: clearLogoPath ?? this.clearLogoPath,
|
||||
backgroundSquarePath: backgroundSquarePath ?? this.backgroundSquarePath,
|
||||
durationMs: durationMs ?? this.durationMs,
|
||||
viewOffsetMs: viewOffsetMs ?? this.viewOffsetMs,
|
||||
viewCount: viewCount ?? this.viewCount,
|
||||
lastViewedAt: lastViewedAt ?? this.lastViewedAt,
|
||||
leafCount: leafCount ?? this.leafCount,
|
||||
viewedLeafCount: viewedLeafCount ?? this.viewedLeafCount,
|
||||
childCount: childCount ?? this.childCount,
|
||||
addedAt: addedAt ?? this.addedAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
rating: rating ?? this.rating,
|
||||
audienceRating: audienceRating ?? this.audienceRating,
|
||||
userRating: userRating ?? this.userRating,
|
||||
ratingImage: ratingImage ?? this.ratingImage,
|
||||
audienceRatingImage: audienceRatingImage ?? this.audienceRatingImage,
|
||||
genres: genres ?? this.genres,
|
||||
directors: directors ?? this.directors,
|
||||
writers: writers ?? this.writers,
|
||||
producers: producers ?? this.producers,
|
||||
countries: countries ?? this.countries,
|
||||
collections: collections ?? this.collections,
|
||||
labels: labels ?? this.labels,
|
||||
styles: styles ?? this.styles,
|
||||
moods: moods ?? this.moods,
|
||||
roles: roles ?? this.roles,
|
||||
mediaVersions: mediaVersions ?? this.mediaVersions,
|
||||
libraryId: libraryId ?? this.libraryId,
|
||||
libraryTitle: libraryTitle ?? this.libraryTitle,
|
||||
audioLanguage: audioLanguage ?? this.audioLanguage,
|
||||
subtitleLanguage: subtitleLanguage ?? this.subtitleLanguage,
|
||||
subtitleMode: subtitleMode ?? this.subtitleMode,
|
||||
trailerKey: trailerKey ?? this.trailerKey,
|
||||
playlistItemId: playlistItemId ?? this.playlistItemId,
|
||||
playQueueItemId: playQueueItemId ?? this.playQueueItemId,
|
||||
subtype: subtype ?? this.subtype,
|
||||
extraType: extraType ?? this.extraType,
|
||||
serverId: serverId ?? this.serverId,
|
||||
serverName: serverName ?? this.serverName,
|
||||
raw: raw ?? this.raw,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
...super.toJson(),
|
||||
if (editionTitle != null) 'editionTitle': editionTitle,
|
||||
if (audienceRating != null) 'audienceRating': audienceRating,
|
||||
if (ratingImage != null) 'ratingImage': ratingImage,
|
||||
if (audienceRatingImage != null) 'audienceRatingImage': audienceRatingImage,
|
||||
if (subtitleLanguage != null) 'subtitleLanguage': subtitleLanguage,
|
||||
if (subtitleMode != null) 'subtitleMode': subtitleMode,
|
||||
if (trailerKey != null) 'trailerKey': trailerKey,
|
||||
if (playlistItemId != null) 'playlistItemId': playlistItemId,
|
||||
if (playQueueItemId != null) 'playQueueItemId': playQueueItemId,
|
||||
if (subtype != null) 'subtype': subtype,
|
||||
if (extraType != null) 'extraType': extraType,
|
||||
};
|
||||
}
|
||||
|
||||
/// Restore a [PlexMediaItem] from a [toJson] payload. Reads the Plex-only
|
||||
/// keys on top of the backend-neutral fields parsed by [_parseBaseFields].
|
||||
factory PlexMediaItem.fromJson(Map<String, dynamic> json) {
|
||||
final base = _parseBaseFields(json);
|
||||
return PlexMediaItem(
|
||||
id: base.id,
|
||||
kind: base.kind,
|
||||
guid: base.guid,
|
||||
title: base.title,
|
||||
titleSort: base.titleSort,
|
||||
summary: base.summary,
|
||||
tagline: base.tagline,
|
||||
originalTitle: base.originalTitle,
|
||||
editionTitle: json['editionTitle'] as String?,
|
||||
studio: base.studio,
|
||||
year: base.year,
|
||||
originallyAvailableAt: base.originallyAvailableAt,
|
||||
contentRating: base.contentRating,
|
||||
parentId: base.parentId,
|
||||
parentTitle: base.parentTitle,
|
||||
parentThumbPath: base.parentThumbPath,
|
||||
parentIndex: base.parentIndex,
|
||||
index: base.index,
|
||||
grandparentId: base.grandparentId,
|
||||
grandparentTitle: base.grandparentTitle,
|
||||
grandparentThumbPath: base.grandparentThumbPath,
|
||||
grandparentArtPath: base.grandparentArtPath,
|
||||
thumbPath: base.thumbPath,
|
||||
artPath: base.artPath,
|
||||
clearLogoPath: base.clearLogoPath,
|
||||
backgroundSquarePath: base.backgroundSquarePath,
|
||||
durationMs: base.durationMs,
|
||||
viewOffsetMs: base.viewOffsetMs,
|
||||
viewCount: base.viewCount,
|
||||
lastViewedAt: base.lastViewedAt,
|
||||
leafCount: base.leafCount,
|
||||
viewedLeafCount: base.viewedLeafCount,
|
||||
childCount: base.childCount,
|
||||
addedAt: base.addedAt,
|
||||
updatedAt: base.updatedAt,
|
||||
rating: base.rating,
|
||||
audienceRating: flexibleDouble(json['audienceRating']),
|
||||
userRating: base.userRating,
|
||||
ratingImage: json['ratingImage'] as String?,
|
||||
audienceRatingImage: json['audienceRatingImage'] as String?,
|
||||
genres: base.genres,
|
||||
directors: base.directors,
|
||||
writers: base.writers,
|
||||
producers: base.producers,
|
||||
countries: base.countries,
|
||||
collections: base.collections,
|
||||
labels: base.labels,
|
||||
styles: base.styles,
|
||||
moods: base.moods,
|
||||
roles: base.roles,
|
||||
mediaVersions: base.mediaVersions,
|
||||
libraryId: base.libraryId,
|
||||
libraryTitle: base.libraryTitle,
|
||||
audioLanguage: base.audioLanguage,
|
||||
subtitleLanguage: json['subtitleLanguage'] as String?,
|
||||
subtitleMode: flexibleInt(json['subtitleMode']),
|
||||
trailerKey: json['trailerKey'] as String?,
|
||||
playlistItemId: flexibleInt(json['playlistItemId']),
|
||||
playQueueItemId: flexibleInt(json['playQueueItemId']),
|
||||
subtype: json['subtype'] as String?,
|
||||
extraType: flexibleInt(json['extraType']),
|
||||
serverId: base.serverId,
|
||||
serverName: base.serverName,
|
||||
raw: base.raw,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../utils/json_utils.dart';
|
||||
import 'media_stream.dart';
|
||||
|
||||
part 'media_part.g.dart';
|
||||
|
||||
/// One physical file part of a [MediaVersion]. A movie typically has a single
|
||||
/// part; some Plex multi-part files (CD1/CD2) and DVD/BluRay rips can have
|
||||
/// several. Jellyfin items always map to a single part per media source.
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class MediaPart {
|
||||
/// Backend-opaque part identifier.
|
||||
@JsonKey(fromJson: _stringFromJson)
|
||||
final String id;
|
||||
|
||||
/// Backend-specific path used to construct a direct stream URL — e.g. Plex's
|
||||
@@ -13,11 +20,14 @@ class MediaPart {
|
||||
/// appending auth.
|
||||
final String? streamPath;
|
||||
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? sizeBytes;
|
||||
final String? container;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? durationMs;
|
||||
final bool? accessible;
|
||||
final bool? exists;
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final List<MediaStream> streams;
|
||||
|
||||
const MediaPart({
|
||||
@@ -31,7 +41,13 @@ class MediaPart {
|
||||
this.streams = const [],
|
||||
});
|
||||
|
||||
factory MediaPart.fromJson(Map<String, dynamic> json) => _$MediaPartFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$MediaPartToJson(this);
|
||||
|
||||
/// Defaults to true when fields are absent. Plex sets these only when the
|
||||
/// metadata request includes `checkFiles=1`.
|
||||
bool get isPlayable => accessible != false && exists != false;
|
||||
}
|
||||
|
||||
String _stringFromJson(Object? raw) => (raw ?? '').toString();
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'media_part.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
MediaPart _$MediaPartFromJson(Map<String, dynamic> json) => MediaPart(
|
||||
id: _stringFromJson(json['id']),
|
||||
streamPath: json['streamPath'] as String?,
|
||||
sizeBytes: flexibleInt(json['sizeBytes']),
|
||||
container: json['container'] as String?,
|
||||
durationMs: flexibleInt(json['durationMs']),
|
||||
accessible: json['accessible'] as bool?,
|
||||
exists: json['exists'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MediaPartToJson(MediaPart instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'streamPath': ?instance.streamPath,
|
||||
'sizeBytes': ?instance.sizeBytes,
|
||||
'container': ?instance.container,
|
||||
'durationMs': ?instance.durationMs,
|
||||
'accessible': ?instance.accessible,
|
||||
'exists': ?instance.exists,
|
||||
};
|
||||
@@ -1,9 +1,21 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'media_role.g.dart';
|
||||
|
||||
/// A cast or crew member attached to a media item.
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class MediaRole {
|
||||
final String? id;
|
||||
@JsonKey(fromJson: _stringFromJson)
|
||||
final String tag;
|
||||
final String? role;
|
||||
final String? thumbPath;
|
||||
|
||||
const MediaRole({this.id, required this.tag, this.role, this.thumbPath});
|
||||
|
||||
factory MediaRole.fromJson(Map<String, dynamic> json) => _$MediaRoleFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$MediaRoleToJson(this);
|
||||
}
|
||||
|
||||
String _stringFromJson(Object? raw) => (raw ?? '').toString();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'media_role.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
MediaRole _$MediaRoleFromJson(Map<String, dynamic> json) => MediaRole(
|
||||
id: json['id'] as String?,
|
||||
tag: _stringFromJson(json['tag']),
|
||||
role: json['role'] as String?,
|
||||
thumbPath: json['thumbPath'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MediaRoleToJson(MediaRole instance) => <String, dynamic>{
|
||||
'id': ?instance.id,
|
||||
'tag': instance.tag,
|
||||
'role': ?instance.role,
|
||||
'thumbPath': ?instance.thumbPath,
|
||||
};
|
||||
@@ -1,7 +1,12 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../utils/codec_utils.dart';
|
||||
import '../utils/formatters.dart';
|
||||
import '../utils/json_utils.dart';
|
||||
import 'media_part.dart';
|
||||
|
||||
part 'media_version.g.dart';
|
||||
|
||||
/// Convert backend bitrates reported in bits-per-second to app-standard kbps.
|
||||
int? bitrateKbpsFromBps(int? bps) {
|
||||
if (bps == null || bps <= 0) return null;
|
||||
@@ -19,15 +24,21 @@ String _videoResolutionDisplayLabel(String resolution) {
|
||||
/// A single media variant available for an item — represents one quality level
|
||||
/// or transcode profile of the underlying file. An item with multiple versions
|
||||
/// (e.g. 4K + 1080p re-encode) exposes one [MediaVersion] per option.
|
||||
@JsonSerializable(includeIfNull: false, explicitToJson: true)
|
||||
class MediaVersion {
|
||||
/// Backend-opaque version identifier.
|
||||
@JsonKey(fromJson: _stringFromJson)
|
||||
final String id;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? width;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? height;
|
||||
final String? videoResolution; // "1080", "4k", "sd"
|
||||
final String? videoCodec;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? bitrate;
|
||||
final String? container;
|
||||
@JsonKey(fromJson: _partsFromJson, toJson: _partsToJson)
|
||||
final List<MediaPart> parts;
|
||||
|
||||
/// Human-readable name for this version (e.g. "Director's Cut").
|
||||
@@ -48,6 +59,10 @@ class MediaVersion {
|
||||
this.name,
|
||||
});
|
||||
|
||||
factory MediaVersion.fromJson(Map<String, dynamic> json) => _$MediaVersionFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$MediaVersionToJson(this);
|
||||
|
||||
/// Defaults to true when file-access fields are absent. Plex only populates
|
||||
/// them when metadata is fetched with `checkFiles=1`.
|
||||
bool get isPlayable => parts.isEmpty || parts.first.isPlayable;
|
||||
@@ -123,3 +138,16 @@ class MediaVersion {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _stringFromJson(Object? raw) => (raw ?? '').toString();
|
||||
|
||||
List<MediaPart> _partsFromJson(Object? raw) {
|
||||
return raw is List
|
||||
? [
|
||||
for (final part in raw)
|
||||
if (part is Map<String, dynamic>) MediaPart.fromJson(part),
|
||||
]
|
||||
: const [];
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _partsToJson(List<MediaPart> parts) => [for (final part in parts) part.toJson()];
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'media_version.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
MediaVersion _$MediaVersionFromJson(Map<String, dynamic> json) => MediaVersion(
|
||||
id: _stringFromJson(json['id']),
|
||||
width: flexibleInt(json['width']),
|
||||
height: flexibleInt(json['height']),
|
||||
videoResolution: json['videoResolution'] as String?,
|
||||
videoCodec: json['videoCodec'] as String?,
|
||||
bitrate: flexibleInt(json['bitrate']),
|
||||
container: json['container'] as String?,
|
||||
parts: json['parts'] == null ? const [] : _partsFromJson(json['parts']),
|
||||
name: json['name'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MediaVersionToJson(MediaVersion instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'width': ?instance.width,
|
||||
'height': ?instance.height,
|
||||
'videoResolution': ?instance.videoResolution,
|
||||
'videoCodec': ?instance.videoCodec,
|
||||
'bitrate': ?instance.bitrate,
|
||||
'container': ?instance.container,
|
||||
'parts': _partsToJson(instance.parts),
|
||||
'name': ?instance.name,
|
||||
};
|
||||
@@ -10,43 +10,61 @@ RemoteDevice _$RemoteDeviceFromJson(Map<String, dynamic> json) => RemoteDevice(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
platform: json['platform'] as String,
|
||||
connectedAt: json['connectedAt'] == null ? null : DateTime.parse(json['connectedAt'] as String),
|
||||
capabilities: (json['capabilities'] as Map<String, dynamic>?)?.map((k, e) => MapEntry(k, e as bool)),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'platform': instance.platform,
|
||||
'connectedAt': instance.connectedAt.toIso8601String(),
|
||||
'capabilities': instance.capabilities,
|
||||
};
|
||||
|
||||
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) => RemoteSession(
|
||||
role: $enumDecode(_$RemoteSessionRoleEnumMap, json['role'], unknownValue: RemoteSessionRole.remote),
|
||||
status:
|
||||
$enumDecodeNullable(
|
||||
_$RemoteSessionStatusEnumMap,
|
||||
json['status'],
|
||||
unknownValue: RemoteSessionStatus.disconnected,
|
||||
) ??
|
||||
RemoteSessionStatus.disconnected,
|
||||
connectedDevice: json['connectedDevice'] == null
|
||||
connectedAt: json['connectedAt'] == null
|
||||
? null
|
||||
: RemoteDevice.fromJson(json['connectedDevice'] as Map<String, dynamic>),
|
||||
createdAt: json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String),
|
||||
errorMessage: json['errorMessage'] as String?,
|
||||
: DateTime.parse(json['connectedAt'] as String),
|
||||
capabilities: (json['capabilities'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as bool),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) => <String, dynamic>{
|
||||
'role': _$RemoteSessionRoleEnumMap[instance.role]!,
|
||||
'status': _$RemoteSessionStatusEnumMap[instance.status]!,
|
||||
'connectedDevice': instance.connectedDevice,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'errorMessage': instance.errorMessage,
|
||||
};
|
||||
Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'platform': instance.platform,
|
||||
'connectedAt': instance.connectedAt.toIso8601String(),
|
||||
'capabilities': instance.capabilities,
|
||||
};
|
||||
|
||||
const _$RemoteSessionRoleEnumMap = {RemoteSessionRole.host: 'host', RemoteSessionRole.remote: 'remote'};
|
||||
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) =>
|
||||
RemoteSession(
|
||||
role: $enumDecode(
|
||||
_$RemoteSessionRoleEnumMap,
|
||||
json['role'],
|
||||
unknownValue: RemoteSessionRole.remote,
|
||||
),
|
||||
status:
|
||||
$enumDecodeNullable(
|
||||
_$RemoteSessionStatusEnumMap,
|
||||
json['status'],
|
||||
unknownValue: RemoteSessionStatus.disconnected,
|
||||
) ??
|
||||
RemoteSessionStatus.disconnected,
|
||||
connectedDevice: json['connectedDevice'] == null
|
||||
? null
|
||||
: RemoteDevice.fromJson(
|
||||
json['connectedDevice'] as Map<String, dynamic>,
|
||||
),
|
||||
createdAt: json['createdAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['createdAt'] as String),
|
||||
errorMessage: json['errorMessage'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) =>
|
||||
<String, dynamic>{
|
||||
'role': _$RemoteSessionRoleEnumMap[instance.role]!,
|
||||
'status': _$RemoteSessionStatusEnumMap[instance.status]!,
|
||||
'connectedDevice': instance.connectedDevice,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'errorMessage': instance.errorMessage,
|
||||
};
|
||||
|
||||
const _$RemoteSessionRoleEnumMap = {
|
||||
RemoteSessionRole.host: 'host',
|
||||
RemoteSessionRole.remote: 'remote',
|
||||
};
|
||||
|
||||
const _$RemoteSessionStatusEnumMap = {
|
||||
RemoteSessionStatus.disconnected: 'disconnected',
|
||||
|
||||
@@ -6,24 +6,26 @@ part of 'livetv_channel.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LiveTvChannel _$LiveTvChannelFromJson(Map<String, dynamic> json) => LiveTvChannel(
|
||||
key: _readChannelKey(json, 'key') as String,
|
||||
identifier: _readChannelIdentifier(json, 'identifier') as String?,
|
||||
callSign: json['callSign'] as String?,
|
||||
title: _readChannelTitle(json, 'title') as String?,
|
||||
thumb: json['thumb'] as String?,
|
||||
art: json['art'] as String?,
|
||||
number: _readChannelNumber(json, 'number') as String?,
|
||||
hd: json['hd'] == null ? false : flexibleBool(json['hd']),
|
||||
lineup: json['lineup'] as String?,
|
||||
slug: json['slug'] as String?,
|
||||
drm: flexibleBool(json['drm']),
|
||||
);
|
||||
LiveTvChannel _$LiveTvChannelFromJson(Map<String, dynamic> json) =>
|
||||
LiveTvChannel(
|
||||
key: _readChannelKey(json, 'key') as String,
|
||||
identifier: _readChannelIdentifier(json, 'identifier') as String?,
|
||||
callSign: json['callSign'] as String?,
|
||||
title: _readChannelTitle(json, 'title') as String?,
|
||||
thumb: json['thumb'] as String?,
|
||||
art: json['art'] as String?,
|
||||
number: _readChannelNumber(json, 'number') as String?,
|
||||
hd: json['hd'] == null ? false : flexibleBool(json['hd']),
|
||||
lineup: json['lineup'] as String?,
|
||||
slug: json['slug'] as String?,
|
||||
drm: flexibleBool(json['drm']),
|
||||
);
|
||||
|
||||
FavoriteChannel _$FavoriteChannelFromJson(Map<String, dynamic> json) => FavoriteChannel(
|
||||
source: json['source'] as String? ?? '',
|
||||
id: _readFavoriteChannelId(json, 'id') as String,
|
||||
title: json['title'] as String?,
|
||||
thumb: json['thumb'] as String?,
|
||||
vcn: json['vcn'] as String?,
|
||||
);
|
||||
FavoriteChannel _$FavoriteChannelFromJson(Map<String, dynamic> json) =>
|
||||
FavoriteChannel(
|
||||
source: json['source'] as String? ?? '',
|
||||
id: _readFavoriteChannelId(json, 'id') as String,
|
||||
title: json['title'] as String?,
|
||||
thumb: json['thumb'] as String?,
|
||||
vcn: json['vcn'] as String?,
|
||||
);
|
||||
|
||||
@@ -25,14 +25,19 @@ LiveTvDvr _$LiveTvDvrFromJson(Map<String, dynamic> json) => LiveTvDvr(
|
||||
sources: json['sources'] as String?,
|
||||
uri: json['uri'] as String?,
|
||||
lastSeenAt: flexibleInt(json['lastSeenAt']),
|
||||
channelMappings: json['ChannelMapping'] == null ? const [] : _parseChannelMappings(json['ChannelMapping']),
|
||||
settings: json['Setting'] == null ? const [] : _parseSettings(json['Setting']),
|
||||
channelMappings: json['ChannelMapping'] == null
|
||||
? const []
|
||||
: _parseChannelMappings(json['ChannelMapping']),
|
||||
settings: json['Setting'] == null
|
||||
? const []
|
||||
: _parseSettings(json['Setting']),
|
||||
devices: json['Device'] == null ? const [] : _parseRawMaps(json['Device']),
|
||||
);
|
||||
|
||||
ChannelMapping _$ChannelMappingFromJson(Map<String, dynamic> json) => ChannelMapping(
|
||||
channelKey: json['channelKey'] as String?,
|
||||
deviceIdentifier: json['deviceIdentifier'] as String?,
|
||||
enabled: flexibleBool(json['enabled']),
|
||||
lineupIdentifier: json['lineupIdentifier'] as String?,
|
||||
);
|
||||
ChannelMapping _$ChannelMappingFromJson(Map<String, dynamic> json) =>
|
||||
ChannelMapping(
|
||||
channelKey: json['channelKey'] as String?,
|
||||
deviceIdentifier: json['deviceIdentifier'] as String?,
|
||||
enabled: flexibleBool(json['enabled']),
|
||||
lineupIdentifier: json['lineupIdentifier'] as String?,
|
||||
);
|
||||
|
||||
@@ -6,19 +6,23 @@ part of 'livetv_lineup.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LiveTvCountry _$LiveTvCountryFromJson(Map<String, dynamic> json) => LiveTvCountry(
|
||||
key: json['key'] as String?,
|
||||
type: json['type'] as String?,
|
||||
title: json['title'] as String? ?? '',
|
||||
code: json['code'] as String? ?? '',
|
||||
language: json['language'] as String?,
|
||||
languageTitle: json['languageTitle'] as String?,
|
||||
example: json['example'] as String?,
|
||||
flavor: flexibleInt(json['flavor']),
|
||||
);
|
||||
LiveTvCountry _$LiveTvCountryFromJson(Map<String, dynamic> json) =>
|
||||
LiveTvCountry(
|
||||
key: json['key'] as String?,
|
||||
type: json['type'] as String?,
|
||||
title: json['title'] as String? ?? '',
|
||||
code: json['code'] as String? ?? '',
|
||||
language: json['language'] as String?,
|
||||
languageTitle: json['languageTitle'] as String?,
|
||||
example: json['example'] as String?,
|
||||
flavor: flexibleInt(json['flavor']),
|
||||
);
|
||||
|
||||
LiveTvLanguage _$LiveTvLanguageFromJson(Map<String, dynamic> json) =>
|
||||
LiveTvLanguage(code: json['code'] as String? ?? '', title: json['title'] as String? ?? '');
|
||||
LiveTvLanguage(
|
||||
code: json['code'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
);
|
||||
|
||||
LiveTvRegion _$LiveTvRegionFromJson(Map<String, dynamic> json) => LiveTvRegion(
|
||||
key: json['key'] as String? ?? '',
|
||||
@@ -32,5 +36,7 @@ LiveTvLineup _$LiveTvLineupFromJson(Map<String, dynamic> json) => LiveTvLineup(
|
||||
title: json['title'] as String?,
|
||||
lineupType: flexibleInt(json['lineupType']),
|
||||
location: json['location'] as String?,
|
||||
channels: json['Channel'] == null ? const [] : _parseChannels(json['Channel']),
|
||||
channels: json['Channel'] == null
|
||||
? const []
|
||||
: _parseChannels(json['Channel']),
|
||||
);
|
||||
|
||||
@@ -6,8 +6,9 @@ part of 'livetv_server_status.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LiveTvServerStatus _$LiveTvServerStatusFromJson(Map<String, dynamic> json) => LiveTvServerStatus(
|
||||
liveTvCount: flexibleInt(json['livetv']),
|
||||
allowTuners: flexibleBoolNullable(json['allowTuners']),
|
||||
ownerFeatures: json['ownerFeatures'] as String?,
|
||||
);
|
||||
LiveTvServerStatus _$LiveTvServerStatusFromJson(Map<String, dynamic> json) =>
|
||||
LiveTvServerStatus(
|
||||
liveTvCount: flexibleInt(json['livetv']),
|
||||
allowTuners: flexibleBoolNullable(json['allowTuners']),
|
||||
ownerFeatures: json['ownerFeatures'] as String?,
|
||||
);
|
||||
|
||||
@@ -6,19 +6,20 @@ part of 'livetv_session.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LiveTvSession _$LiveTvSessionFromJson(Map<String, dynamic> json) => LiveTvSession(
|
||||
sessionID: readStringField(json, 'sessionID') as String? ?? '',
|
||||
dvrID: readStringField(json, 'dvrID') as String?,
|
||||
channelIdentifier: json['channelIdentifier'] as String?,
|
||||
channelCallSign: json['channelCallSign'] as String?,
|
||||
channelTitle: json['channelTitle'] as String?,
|
||||
activityUUID: json['activityUUID'] as String?,
|
||||
currentPosition: flexibleInt(json['currentPosition']),
|
||||
nextPosition: flexibleInt(json['nextPosition']),
|
||||
startedAt: flexibleInt(json['startedAt']),
|
||||
captureBuffer: _captureBufferFromRaw(json['CaptureBuffer']),
|
||||
grabOperation: _grabOperationFromRaw(json['MediaGrabOperation']),
|
||||
timeline: _firstMap(json['Timeline']),
|
||||
airingMetadataItem: _programFromRaw(json['AiringMetadataItem']),
|
||||
upNextMetadataItem: _programFromRaw(json['UpNextMetadataItem']),
|
||||
);
|
||||
LiveTvSession _$LiveTvSessionFromJson(Map<String, dynamic> json) =>
|
||||
LiveTvSession(
|
||||
sessionID: readStringField(json, 'sessionID') as String? ?? '',
|
||||
dvrID: readStringField(json, 'dvrID') as String?,
|
||||
channelIdentifier: json['channelIdentifier'] as String?,
|
||||
channelCallSign: json['channelCallSign'] as String?,
|
||||
channelTitle: json['channelTitle'] as String?,
|
||||
activityUUID: json['activityUUID'] as String?,
|
||||
currentPosition: flexibleInt(json['currentPosition']),
|
||||
nextPosition: flexibleInt(json['nextPosition']),
|
||||
startedAt: flexibleInt(json['startedAt']),
|
||||
captureBuffer: _captureBufferFromRaw(json['CaptureBuffer']),
|
||||
grabOperation: _grabOperationFromRaw(json['MediaGrabOperation']),
|
||||
timeline: _firstMap(json['Timeline']),
|
||||
airingMetadataItem: _programFromRaw(json['AiringMetadataItem']),
|
||||
upNextMetadataItem: _programFromRaw(json['UpNextMetadataItem']),
|
||||
);
|
||||
|
||||
@@ -6,19 +6,20 @@ part of 'media_grab_operation.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
MediaGrabOperation _$MediaGrabOperationFromJson(Map<String, dynamic> json) => MediaGrabOperation(
|
||||
mediaSubscriptionID: flexibleInt(json['mediaSubscriptionID']),
|
||||
mediaIndex: flexibleInt(json['mediaIndex']),
|
||||
id: json['id'] as String? ?? '',
|
||||
key: json['key'] as String?,
|
||||
grabberIdentifier: json['grabberIdentifier'] as String?,
|
||||
grabberProtocol: json['grabberProtocol'] as String?,
|
||||
percent: flexibleDouble(json['percent']),
|
||||
currentSize: flexibleInt(json['currentSize']),
|
||||
status: json['status'] as String?,
|
||||
provider: json['provider'] as String?,
|
||||
rolling: flexibleBoolNullable(json['rolling']),
|
||||
error: json['error'] as String?,
|
||||
linkedKey: json['linkedKey'] as String?,
|
||||
metadata: _metadataFromJson(json['Metadata']),
|
||||
);
|
||||
MediaGrabOperation _$MediaGrabOperationFromJson(Map<String, dynamic> json) =>
|
||||
MediaGrabOperation(
|
||||
mediaSubscriptionID: flexibleInt(json['mediaSubscriptionID']),
|
||||
mediaIndex: flexibleInt(json['mediaIndex']),
|
||||
id: json['id'] as String? ?? '',
|
||||
key: json['key'] as String?,
|
||||
grabberIdentifier: json['grabberIdentifier'] as String?,
|
||||
grabberProtocol: json['grabberProtocol'] as String?,
|
||||
percent: flexibleDouble(json['percent']),
|
||||
currentSize: flexibleInt(json['currentSize']),
|
||||
status: json['status'] as String?,
|
||||
provider: json['provider'] as String?,
|
||||
rolling: flexibleBoolNullable(json['rolling']),
|
||||
error: json['error'] as String?,
|
||||
linkedKey: json['linkedKey'] as String?,
|
||||
metadata: _metadataFromJson(json['Metadata']),
|
||||
);
|
||||
|
||||
@@ -12,26 +12,33 @@ MediaGrabber _$MediaGrabberFromJson(Map<String, dynamic> json) => MediaGrabber(
|
||||
title: json['title'] as String?,
|
||||
);
|
||||
|
||||
MediaGrabberDevice _$MediaGrabberDeviceFromJson(Map<String, dynamic> json) => MediaGrabberDevice(
|
||||
key: json['key'] as String? ?? '',
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
uri: json['uri'] as String?,
|
||||
protocol: json['protocol'] as String?,
|
||||
title: json['title'] as String?,
|
||||
make: json['make'] as String?,
|
||||
model: json['model'] as String?,
|
||||
modelNumber: json['modelNumber'] as String?,
|
||||
firmware: json['firmware'] as String?,
|
||||
tuners: flexibleInt(json['tuners']),
|
||||
sources: json['sources'] as String?,
|
||||
status: flexibleInt(json['status']),
|
||||
state: flexibleInt(json['state']),
|
||||
lastSeenAt: flexibleInt(json['lastSeenAt']),
|
||||
channelMappings: json['ChannelMapping'] == null ? const [] : _parseChannelMappings(json['ChannelMapping']),
|
||||
settings: json['Setting'] == null ? const [] : _parseSettings(json['Setting']),
|
||||
);
|
||||
MediaGrabberDevice _$MediaGrabberDeviceFromJson(Map<String, dynamic> json) =>
|
||||
MediaGrabberDevice(
|
||||
key: json['key'] as String? ?? '',
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
uri: json['uri'] as String?,
|
||||
protocol: json['protocol'] as String?,
|
||||
title: json['title'] as String?,
|
||||
make: json['make'] as String?,
|
||||
model: json['model'] as String?,
|
||||
modelNumber: json['modelNumber'] as String?,
|
||||
firmware: json['firmware'] as String?,
|
||||
tuners: flexibleInt(json['tuners']),
|
||||
sources: json['sources'] as String?,
|
||||
status: flexibleInt(json['status']),
|
||||
state: flexibleInt(json['state']),
|
||||
lastSeenAt: flexibleInt(json['lastSeenAt']),
|
||||
channelMappings: json['ChannelMapping'] == null
|
||||
? const []
|
||||
: _parseChannelMappings(json['ChannelMapping']),
|
||||
settings: json['Setting'] == null
|
||||
? const []
|
||||
: _parseSettings(json['Setting']),
|
||||
);
|
||||
|
||||
MediaGrabberDeviceChannel _$MediaGrabberDeviceChannelFromJson(Map<String, dynamic> json) => MediaGrabberDeviceChannel(
|
||||
MediaGrabberDeviceChannel _$MediaGrabberDeviceChannelFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => MediaGrabberDeviceChannel(
|
||||
identifier: readStringField(json, 'identifier') as String? ?? '',
|
||||
key: readStringField(json, 'key') as String?,
|
||||
name: readStringField(json, 'name') as String?,
|
||||
|
||||
@@ -6,26 +6,33 @@ part of 'media_provider_info.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
MediaProviderInfo _$MediaProviderInfoFromJson(Map<String, dynamic> json) => MediaProviderInfo(
|
||||
id: flexibleInt(json['id']),
|
||||
parentID: flexibleInt(json['parentID']),
|
||||
identifier: json['identifier'] as String? ?? '',
|
||||
providerIdentifier: json['providerIdentifier'] as String?,
|
||||
title: json['title'] as String?,
|
||||
types: json['types'] as String?,
|
||||
protocols: json['protocols'] as String?,
|
||||
epgSource: json['epgSource'] as String?,
|
||||
friendlyName: json['friendlyName'] as String?,
|
||||
features: json['Feature'] == null ? const [] : _parseFeatures(json['Feature']),
|
||||
);
|
||||
MediaProviderInfo _$MediaProviderInfoFromJson(Map<String, dynamic> json) =>
|
||||
MediaProviderInfo(
|
||||
id: flexibleInt(json['id']),
|
||||
parentID: flexibleInt(json['parentID']),
|
||||
identifier: json['identifier'] as String? ?? '',
|
||||
providerIdentifier: json['providerIdentifier'] as String?,
|
||||
title: json['title'] as String?,
|
||||
types: json['types'] as String?,
|
||||
protocols: json['protocols'] as String?,
|
||||
epgSource: json['epgSource'] as String?,
|
||||
friendlyName: json['friendlyName'] as String?,
|
||||
features: json['Feature'] == null
|
||||
? const []
|
||||
: _parseFeatures(json['Feature']),
|
||||
);
|
||||
|
||||
MediaProviderFeature _$MediaProviderFeatureFromJson(Map<String, dynamic> json) => MediaProviderFeature(
|
||||
MediaProviderFeature _$MediaProviderFeatureFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => MediaProviderFeature(
|
||||
key: json['key'] as String?,
|
||||
type: json['type'] as String? ?? '',
|
||||
flavor: json['flavor'] as String?,
|
||||
scrobbleKey: json['scrobbleKey'] as String?,
|
||||
unscrobbleKey: json['unscrobbleKey'] as String?,
|
||||
directories: json['Directory'] == null ? const [] : _parseRawMaps(json['Directory']),
|
||||
directories: json['Directory'] == null
|
||||
? const []
|
||||
: _parseRawMaps(json['Directory']),
|
||||
actions: json['Action'] == null ? const [] : _parseRawMaps(json['Action']),
|
||||
pivots: json['Pivot'] == null ? const [] : _parseRawMaps(json['Pivot']),
|
||||
);
|
||||
|
||||
@@ -6,41 +6,53 @@ part of 'media_subscription.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SubscriptionTemplate _$SubscriptionTemplateFromJson(Map<String, dynamic> json) => SubscriptionTemplate(
|
||||
subscriptions: json['MediaSubscription'] == null ? const [] : _parseSubscriptions(json['MediaSubscription']),
|
||||
SubscriptionTemplate _$SubscriptionTemplateFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => SubscriptionTemplate(
|
||||
subscriptions: json['MediaSubscription'] == null
|
||||
? const []
|
||||
: _parseSubscriptions(json['MediaSubscription']),
|
||||
);
|
||||
|
||||
MediaSubscription _$MediaSubscriptionFromJson(Map<String, dynamic> json) => MediaSubscription(
|
||||
key: json['key'] as String? ?? '',
|
||||
type: flexibleInt(json['type']),
|
||||
provider: json['provider'] as String?,
|
||||
targetLibrarySectionID: flexibleInt(json['targetLibrarySectionID']),
|
||||
targetSectionLocationID: flexibleInt(json['targetSectionLocationID']),
|
||||
title: json['title'] as String?,
|
||||
selected: flexibleBoolNullable(json['selected']),
|
||||
parameters: json['parameters'] as String?,
|
||||
createdAt: flexibleInt(json['createdAt']),
|
||||
storageTotal: flexibleInt(json['storageTotal']),
|
||||
durationTotal: flexibleInt(json['durationTotal']),
|
||||
airingsType: json['airingsType'] as String?,
|
||||
librarySectionTitle: json['librarySectionTitle'] as String?,
|
||||
locationPath: json['locationPath'] as String?,
|
||||
video: _mapFromJson(json['Video']),
|
||||
directory: _mapFromJson(json['Directory']),
|
||||
playlist: _mapFromJson(json['Playlist']),
|
||||
settings: json['Setting'] == null ? const [] : _parseSettings(json['Setting']),
|
||||
grabOperations: json['MediaGrabOperation'] == null ? const [] : _parseGrabOperations(json['MediaGrabOperation']),
|
||||
);
|
||||
MediaSubscription _$MediaSubscriptionFromJson(Map<String, dynamic> json) =>
|
||||
MediaSubscription(
|
||||
key: json['key'] as String? ?? '',
|
||||
type: flexibleInt(json['type']),
|
||||
provider: json['provider'] as String?,
|
||||
targetLibrarySectionID: flexibleInt(json['targetLibrarySectionID']),
|
||||
targetSectionLocationID: flexibleInt(json['targetSectionLocationID']),
|
||||
title: json['title'] as String?,
|
||||
selected: flexibleBoolNullable(json['selected']),
|
||||
parameters: json['parameters'] as String?,
|
||||
createdAt: flexibleInt(json['createdAt']),
|
||||
storageTotal: flexibleInt(json['storageTotal']),
|
||||
durationTotal: flexibleInt(json['durationTotal']),
|
||||
airingsType: json['airingsType'] as String?,
|
||||
librarySectionTitle: json['librarySectionTitle'] as String?,
|
||||
locationPath: json['locationPath'] as String?,
|
||||
video: _mapFromJson(json['Video']),
|
||||
directory: _mapFromJson(json['Directory']),
|
||||
playlist: _mapFromJson(json['Playlist']),
|
||||
settings: json['Setting'] == null
|
||||
? const []
|
||||
: _parseSettings(json['Setting']),
|
||||
grabOperations: json['MediaGrabOperation'] == null
|
||||
? const []
|
||||
: _parseGrabOperations(json['MediaGrabOperation']),
|
||||
);
|
||||
|
||||
SubscriptionSetting _$SubscriptionSettingFromJson(Map<String, dynamic> json) => SubscriptionSetting(
|
||||
id: json['id'] as String? ?? '',
|
||||
label: json['label'] as String?,
|
||||
summary: json['summary'] as String?,
|
||||
type: json['type'] as String?,
|
||||
defaultValue: json['default'],
|
||||
value: json['value'],
|
||||
hidden: json['hidden'] == null ? false : flexibleBool(json['hidden']),
|
||||
advanced: json['advanced'] == null ? false : flexibleBool(json['advanced']),
|
||||
group: json['group'] as String?,
|
||||
enumValues: json['enumValues'] as String?,
|
||||
);
|
||||
SubscriptionSetting _$SubscriptionSettingFromJson(Map<String, dynamic> json) =>
|
||||
SubscriptionSetting(
|
||||
id: json['id'] as String? ?? '',
|
||||
label: json['label'] as String?,
|
||||
summary: json['summary'] as String?,
|
||||
type: json['type'] as String?,
|
||||
defaultValue: json['default'],
|
||||
value: json['value'],
|
||||
hidden: json['hidden'] == null ? false : flexibleBool(json['hidden']),
|
||||
advanced: json['advanced'] == null
|
||||
? false
|
||||
: flexibleBool(json['advanced']),
|
||||
group: json['group'] as String?,
|
||||
enumValues: json['enumValues'] as String?,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,11 @@ PlexHome _$PlexHomeFromJson(Map<String, dynamic> json) => PlexHome(
|
||||
guestUserUUID: json['guestUserUUID'] as String? ?? '',
|
||||
guestEnabled: json['guestEnabled'] as bool? ?? false,
|
||||
subscription: json['subscription'] as bool? ?? false,
|
||||
users: (json['users'] as List<dynamic>?)?.map((e) => PlexHomeUser.fromJson(e as Map<String, dynamic>)).toList() ?? [],
|
||||
users:
|
||||
(json['users'] as List<dynamic>?)
|
||||
?.map((e) => PlexHomeUser.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlexHomeToJson(PlexHome instance) => <String, dynamic>{
|
||||
|
||||
@@ -22,18 +22,19 @@ PlexHomeUser _$PlexHomeUserFromJson(Map<String, dynamic> json) => PlexHomeUser(
|
||||
protected: json['protected'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlexHomeUserToJson(PlexHomeUser instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'title': instance.title,
|
||||
'username': instance.username,
|
||||
'email': instance.email,
|
||||
'friendlyName': instance.friendlyName,
|
||||
'thumb': instance.thumb,
|
||||
'hasPassword': instance.hasPassword,
|
||||
'restricted': instance.restricted,
|
||||
'updatedAt': instance.updatedAt,
|
||||
'admin': instance.admin,
|
||||
'guest': instance.guest,
|
||||
'protected': instance.protected,
|
||||
};
|
||||
Map<String, dynamic> _$PlexHomeUserToJson(PlexHomeUser instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'title': instance.title,
|
||||
'username': instance.username,
|
||||
'email': instance.email,
|
||||
'friendlyName': instance.friendlyName,
|
||||
'thumb': instance.thumb,
|
||||
'hasPassword': instance.hasPassword,
|
||||
'restricted': instance.restricted,
|
||||
'updatedAt': instance.updatedAt,
|
||||
'admin': instance.admin,
|
||||
'guest': instance.guest,
|
||||
'protected': instance.protected,
|
||||
};
|
||||
|
||||
@@ -6,24 +6,26 @@ part of 'plex_match_result.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
PlexMatchResult _$PlexMatchResultFromJson(Map<String, dynamic> json) => PlexMatchResult(
|
||||
guid: readStringField(json, 'guid') as String? ?? '',
|
||||
name: readStringField(json, 'name') as String? ?? '',
|
||||
year: flexibleInt(json['year']),
|
||||
score: flexibleInt(json['score']),
|
||||
thumb: readStringField(json, 'thumb') as String?,
|
||||
summary: readStringField(json, 'summary') as String?,
|
||||
type: readStringField(json, 'type') as String?,
|
||||
matched: json['matched'] == null ? false : flexibleBool(json['matched']),
|
||||
);
|
||||
PlexMatchResult _$PlexMatchResultFromJson(Map<String, dynamic> json) =>
|
||||
PlexMatchResult(
|
||||
guid: readStringField(json, 'guid') as String? ?? '',
|
||||
name: readStringField(json, 'name') as String? ?? '',
|
||||
year: flexibleInt(json['year']),
|
||||
score: flexibleInt(json['score']),
|
||||
thumb: readStringField(json, 'thumb') as String?,
|
||||
summary: readStringField(json, 'summary') as String?,
|
||||
type: readStringField(json, 'type') as String?,
|
||||
matched: json['matched'] == null ? false : flexibleBool(json['matched']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlexMatchResultToJson(PlexMatchResult instance) => <String, dynamic>{
|
||||
'guid': instance.guid,
|
||||
'name': instance.name,
|
||||
'year': instance.year,
|
||||
'score': instance.score,
|
||||
'thumb': instance.thumb,
|
||||
'summary': instance.summary,
|
||||
'type': instance.type,
|
||||
'matched': instance.matched,
|
||||
};
|
||||
Map<String, dynamic> _$PlexMatchResultToJson(PlexMatchResult instance) =>
|
||||
<String, dynamic>{
|
||||
'guid': instance.guid,
|
||||
'name': instance.name,
|
||||
'year': instance.year,
|
||||
'score': instance.score,
|
||||
'thumb': instance.thumb,
|
||||
'summary': instance.summary,
|
||||
'type': instance.type,
|
||||
'matched': instance.matched,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,9 @@ part of 'plex_subtitle_search_result.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
PlexSubtitleSearchResult _$PlexSubtitleSearchResultFromJson(Map<String, dynamic> json) => PlexSubtitleSearchResult(
|
||||
PlexSubtitleSearchResult _$PlexSubtitleSearchResultFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => PlexSubtitleSearchResult(
|
||||
id: _flexibleIntOrZero(json['id']),
|
||||
key: readStringField(json, 'key') as String? ?? '',
|
||||
codec: readStringField(json, 'codec') as String?,
|
||||
@@ -16,13 +18,21 @@ PlexSubtitleSearchResult _$PlexSubtitleSearchResultFromJson(Map<String, dynamic>
|
||||
providerTitle: readStringField(json, 'providerTitle') as String?,
|
||||
title: readStringField(json, 'title') as String?,
|
||||
displayTitle: readStringField(json, 'displayTitle') as String?,
|
||||
hearingImpaired: json['hearingImpaired'] == null ? false : flexibleBool(json['hearingImpaired']),
|
||||
perfectMatch: json['perfectMatch'] == null ? false : flexibleBool(json['perfectMatch']),
|
||||
downloaded: json['downloaded'] == null ? false : flexibleBool(json['downloaded']),
|
||||
hearingImpaired: json['hearingImpaired'] == null
|
||||
? false
|
||||
: flexibleBool(json['hearingImpaired']),
|
||||
perfectMatch: json['perfectMatch'] == null
|
||||
? false
|
||||
: flexibleBool(json['perfectMatch']),
|
||||
downloaded: json['downloaded'] == null
|
||||
? false
|
||||
: flexibleBool(json['downloaded']),
|
||||
forced: json['forced'] == null ? false : flexibleBool(json['forced']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlexSubtitleSearchResultToJson(PlexSubtitleSearchResult instance) => <String, dynamic>{
|
||||
Map<String, dynamic> _$PlexSubtitleSearchResultToJson(
|
||||
PlexSubtitleSearchResult instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'key': instance.key,
|
||||
'codec': instance.codec,
|
||||
|
||||
@@ -6,32 +6,44 @@ part of 'plex_user_profile.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
PlexUserProfile _$PlexUserProfileFromJson(Map<String, dynamic> json) => PlexUserProfile(
|
||||
PlexUserProfile _$PlexUserProfileFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => PlexUserProfile(
|
||||
autoSelectAudio: json['autoSelectAudio'] as bool? ?? true,
|
||||
defaultAudioAccessibility: (json['defaultAudioAccessibility'] as num?)?.toInt() ?? 0,
|
||||
defaultAudioAccessibility:
|
||||
(json['defaultAudioAccessibility'] as num?)?.toInt() ?? 0,
|
||||
defaultAudioLanguage: json['defaultAudioLanguage'] as String?,
|
||||
defaultAudioLanguages: (json['defaultAudioLanguages'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
defaultAudioLanguages: (json['defaultAudioLanguages'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
defaultSubtitleLanguage: json['defaultSubtitleLanguage'] as String?,
|
||||
defaultSubtitleLanguages: (json['defaultSubtitleLanguages'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
defaultSubtitleLanguages: (json['defaultSubtitleLanguages'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
autoSelectSubtitle: (json['autoSelectSubtitle'] as num?)?.toInt() ?? 0,
|
||||
defaultSubtitleAccessibility: (json['defaultSubtitleAccessibility'] as num?)?.toInt() ?? 0,
|
||||
defaultSubtitleAccessibility:
|
||||
(json['defaultSubtitleAccessibility'] as num?)?.toInt() ?? 0,
|
||||
defaultSubtitleForced: (json['defaultSubtitleForced'] as num?)?.toInt() ?? 1,
|
||||
watchedIndicator: (json['watchedIndicator'] as num?)?.toInt() ?? 1,
|
||||
mediaReviewsVisibility: (json['mediaReviewsVisibility'] as num?)?.toInt() ?? 0,
|
||||
mediaReviewsLanguages: (json['mediaReviewsLanguages'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
mediaReviewsVisibility:
|
||||
(json['mediaReviewsVisibility'] as num?)?.toInt() ?? 0,
|
||||
mediaReviewsLanguages: (json['mediaReviewsLanguages'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlexUserProfileToJson(PlexUserProfile instance) => <String, dynamic>{
|
||||
'autoSelectAudio': instance.autoSelectAudio,
|
||||
'defaultAudioAccessibility': instance.defaultAudioAccessibility,
|
||||
'defaultAudioLanguage': instance.defaultAudioLanguage,
|
||||
'defaultAudioLanguages': instance.defaultAudioLanguages,
|
||||
'defaultSubtitleLanguage': instance.defaultSubtitleLanguage,
|
||||
'defaultSubtitleLanguages': instance.defaultSubtitleLanguages,
|
||||
'autoSelectSubtitle': instance.autoSelectSubtitle,
|
||||
'defaultSubtitleAccessibility': instance.defaultSubtitleAccessibility,
|
||||
'defaultSubtitleForced': instance.defaultSubtitleForced,
|
||||
'watchedIndicator': instance.watchedIndicator,
|
||||
'mediaReviewsVisibility': instance.mediaReviewsVisibility,
|
||||
'mediaReviewsLanguages': instance.mediaReviewsLanguages,
|
||||
};
|
||||
Map<String, dynamic> _$PlexUserProfileToJson(PlexUserProfile instance) =>
|
||||
<String, dynamic>{
|
||||
'autoSelectAudio': instance.autoSelectAudio,
|
||||
'defaultAudioAccessibility': instance.defaultAudioAccessibility,
|
||||
'defaultAudioLanguage': instance.defaultAudioLanguage,
|
||||
'defaultAudioLanguages': instance.defaultAudioLanguages,
|
||||
'defaultSubtitleLanguage': instance.defaultSubtitleLanguage,
|
||||
'defaultSubtitleLanguages': instance.defaultSubtitleLanguages,
|
||||
'autoSelectSubtitle': instance.autoSelectSubtitle,
|
||||
'defaultSubtitleAccessibility': instance.defaultSubtitleAccessibility,
|
||||
'defaultSubtitleForced': instance.defaultSubtitleForced,
|
||||
'watchedIndicator': instance.watchedIndicator,
|
||||
'mediaReviewsVisibility': instance.mediaReviewsVisibility,
|
||||
'mediaReviewsLanguages': instance.mediaReviewsLanguages,
|
||||
};
|
||||
|
||||
@@ -1019,7 +1019,10 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin
|
||||
try {
|
||||
final fullMetadata = await client.fetchItem(metadata.id);
|
||||
if (fullMetadata != null) {
|
||||
metadataToStore = fullMetadata.copyWith(serverId: metadata.serverId, serverName: metadata.serverName);
|
||||
metadataToStore = fullMetadata.copyWith(
|
||||
serverId: metadata.serverId ?? fullMetadata.serverId,
|
||||
serverName: metadata.serverName ?? fullMetadata.serverName,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.w('Failed to fetch full metadata for ${metadata.id}, using partial', error: e);
|
||||
|
||||
@@ -50,10 +50,15 @@ class WatchStateOverlayProvider extends ChangeNotifier with DisposableChangeNoti
|
||||
static MediaItem applyPatch(MediaItem item, WatchStateOverlayPatch? patch) {
|
||||
if (patch == null) return item;
|
||||
|
||||
return item.copyWith(
|
||||
viewCount: patch.isWatched == null ? null : (patch.isWatched! ? 1 : 0),
|
||||
viewOffsetMs: patch.hasViewOffsetMs ? patch.viewOffsetMs : null,
|
||||
);
|
||||
var updated = item;
|
||||
final isWatched = patch.isWatched;
|
||||
if (isWatched != null) {
|
||||
updated = updated.copyWith(viewCount: isWatched ? 1 : 0);
|
||||
}
|
||||
if (patch.hasViewOffsetMs) {
|
||||
updated = updated.copyWith(viewOffsetMs: patch.viewOffsetMs);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
void setActiveProfileId(String? profileId) {
|
||||
|
||||
@@ -229,19 +229,24 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
final isWatched = event.isNowWatched;
|
||||
if (isWatched == null) return;
|
||||
final viewOffsetMs = isWatched && !clearWatchedProgress ? null : 0;
|
||||
MediaItem patchItem(MediaItem item) {
|
||||
final updated = item.copyWith(viewCount: isWatched ? 1 : 0);
|
||||
return viewOffsetMs == null ? updated : updated.copyWith(viewOffsetMs: viewOffsetMs);
|
||||
}
|
||||
|
||||
setStateIfMounted(() {
|
||||
final base = _fullMetadata ?? widget.metadata;
|
||||
if (base.id == event.itemId) {
|
||||
_fullMetadata = base.copyWith(viewCount: isWatched ? 1 : 0, viewOffsetMs: viewOffsetMs);
|
||||
_fullMetadata = patchItem(base);
|
||||
}
|
||||
|
||||
final onDeckEpisode = _onDeckEpisode;
|
||||
if (onDeckEpisode != null && onDeckEpisode.id == event.itemId) {
|
||||
_onDeckEpisode = onDeckEpisode.copyWith(viewCount: isWatched ? 1 : 0, viewOffsetMs: viewOffsetMs);
|
||||
_onDeckEpisode = patchItem(onDeckEpisode);
|
||||
}
|
||||
|
||||
if (epIndex != -1) {
|
||||
final updated = _episodes[epIndex].copyWith(viewCount: isWatched ? 1 : 0, viewOffsetMs: viewOffsetMs);
|
||||
final updated = patchItem(_episodes[epIndex]);
|
||||
_episodes[epIndex] = updated;
|
||||
_syncEpisodeToCache(epIndex, updated);
|
||||
}
|
||||
@@ -272,7 +277,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
|
||||
MediaItem _applyLocalProgress(MediaItem item) {
|
||||
if (!_localProgressById.containsKey(item.id)) return item;
|
||||
return item.copyWith(viewOffsetMs: _localProgressById[item.id]);
|
||||
return item.copyWith(viewOffsetMs: _localProgressById[item.id]!);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -395,10 +400,14 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
final onDeckEpisode = result.onDeckEpisode;
|
||||
if (metadata != null) {
|
||||
setStateIfMounted(() {
|
||||
_fullMetadata = _applyLocalProgress(metadata.copyWith(serverId: serverId, serverName: serverName));
|
||||
_fullMetadata = _applyLocalProgress(
|
||||
metadata.copyWith(serverId: serverId, serverName: serverName ?? metadata.serverName),
|
||||
);
|
||||
_onDeckEpisode = onDeckEpisode == null
|
||||
? null
|
||||
: _applyLocalProgress(onDeckEpisode.copyWith(serverId: serverId, serverName: serverName));
|
||||
: _applyLocalProgress(
|
||||
onDeckEpisode.copyWith(serverId: serverId, serverName: serverName ?? onDeckEpisode.serverName),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -406,7 +415,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
final seasons = await mediaClient.fetchChildren(_metadata.id);
|
||||
_episodeCache.clear();
|
||||
setStateIfMounted(() {
|
||||
_seasons = seasons.map((s) => s.copyWith(serverId: serverId, serverName: serverName)).toList();
|
||||
_seasons = seasons
|
||||
.map((s) => s.copyWith(serverId: serverId, serverName: serverName ?? s.serverName))
|
||||
.toList();
|
||||
});
|
||||
if (_showEpisodesDirectly) {
|
||||
await _fetchAllEpisodes();
|
||||
@@ -992,10 +1003,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
// Preserve serverId from original metadata
|
||||
final serverId = _metadata.serverId;
|
||||
final serverName = _metadata.serverName;
|
||||
final base = _applyLocalProgress((metadata ?? _metadata).copyWith(serverId: serverId, serverName: serverName));
|
||||
final source = metadata ?? _metadata;
|
||||
final base = _applyLocalProgress(
|
||||
source.copyWith(serverId: serverId ?? source.serverId, serverName: serverName ?? source.serverName),
|
||||
);
|
||||
final onDeckWithServerId = onDeckEpisode == null
|
||||
? null
|
||||
: _applyLocalProgress(onDeckEpisode.copyWith(serverId: serverId, serverName: serverName));
|
||||
: _applyLocalProgress(
|
||||
onDeckEpisode.copyWith(
|
||||
serverId: serverId ?? onDeckEpisode.serverId,
|
||||
serverName: serverName ?? onDeckEpisode.serverName,
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_fullMetadata = base;
|
||||
@@ -1065,7 +1084,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
|
||||
// Preserve serverId for each season.
|
||||
final seasonsWithServerId = seasons
|
||||
.map((season) => season.copyWith(serverId: serverId, serverName: _metadata.serverName))
|
||||
.map((season) => season.copyWith(serverId: serverId, serverName: _metadata.serverName ?? season.serverName))
|
||||
.toList();
|
||||
|
||||
// Plex's flattenSeasons modes: 1 = always, 2 = single-season only.
|
||||
@@ -1257,10 +1276,10 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
final episodesWithServerId = episodes
|
||||
.map(
|
||||
(e) => e.copyWith(
|
||||
serverId: _metadata.serverId,
|
||||
serverName: _metadata.serverName,
|
||||
serverId: _metadata.serverId ?? e.serverId,
|
||||
serverName: _metadata.serverName ?? e.serverName,
|
||||
grandparentId: _metadata.id,
|
||||
grandparentTitle: _metadata.title,
|
||||
grandparentTitle: _metadata.title ?? e.grandparentTitle,
|
||||
),
|
||||
)
|
||||
.map(_applyLocalProgress)
|
||||
@@ -1301,7 +1320,12 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
|
||||
// Preserve serverId for each extra (needed for multi-server setups).
|
||||
final extrasWithServerId = extras
|
||||
.map((extra) => extra.copyWith(serverId: _metadata.serverId, serverName: _metadata.serverName))
|
||||
.map(
|
||||
(extra) => extra.copyWith(
|
||||
serverId: _metadata.serverId ?? extra.serverId,
|
||||
serverName: _metadata.serverName ?? extra.serverName,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
setStateIfMounted(() {
|
||||
@@ -1985,7 +2009,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
.map(
|
||||
(e) => e.copyWith(
|
||||
serverId: serverId,
|
||||
serverName: _metadata.serverName,
|
||||
serverName: _metadata.serverName ?? e.serverName,
|
||||
grandparentId: e.grandparentId ?? fallbackGrandparentId,
|
||||
grandparentTitle: e.grandparentTitle ?? fallbackGrandparentTitle,
|
||||
),
|
||||
@@ -2070,7 +2094,10 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
// Play the first episode
|
||||
final firstEpisode = episodes.first;
|
||||
// Preserve serverId for the episode
|
||||
final episodeWithServerId = firstEpisode.copyWith(serverId: _metadata.serverId, serverName: _metadata.serverName);
|
||||
final episodeWithServerId = firstEpisode.copyWith(
|
||||
serverId: _metadata.serverId ?? firstEpisode.serverId,
|
||||
serverName: _metadata.serverName ?? firstEpisode.serverName,
|
||||
);
|
||||
if (mounted) {
|
||||
appLogger.d('Playing first episode: ${episodeWithServerId.title}');
|
||||
await navigateToVideoPlayerWithRefresh(
|
||||
|
||||
@@ -162,7 +162,9 @@ class JellyfinSequentialLauncher extends MediaListPlaybackLauncher {
|
||||
if (raw == null || raw.isEmpty) return const PlayQueueEmpty();
|
||||
|
||||
final shuffled = List.of(raw)..shuffle(Random());
|
||||
final items = shuffled.map((e) => e.copyWith(serverId: serverId, serverName: metadata.serverName)).toList();
|
||||
final items = shuffled
|
||||
.map((e) => e.copyWith(serverId: serverId, serverName: metadata.serverName ?? e.serverName))
|
||||
.toList();
|
||||
|
||||
await dismissLoading();
|
||||
if (!context.mounted && navigateForTesting == null) {
|
||||
|
||||
@@ -286,7 +286,7 @@ class PlexPlayQueueLauncher extends MediaListPlaybackLauncher {
|
||||
var itemToPlay = selectedItem ?? playQueue.items!.first;
|
||||
|
||||
if (copyServerInfo && serverId != null) {
|
||||
itemToPlay = itemToPlay.copyWith(serverId: serverId, serverName: serverName);
|
||||
itemToPlay = itemToPlay.copyWith(serverId: serverId, serverName: serverName ?? itemToPlay.serverName);
|
||||
}
|
||||
|
||||
await navigateToVideoPlayer(context, metadata: itemToPlay);
|
||||
|
||||
+172
-164
@@ -16,51 +16,54 @@ PlexRoleDto _$PlexRoleDtoFromJson(Map<String, dynamic> json) => PlexRoleDto(
|
||||
count: flexibleInt(json['count']),
|
||||
);
|
||||
|
||||
PlexMediaVersionDto _$PlexMediaVersionDtoFromJson(Map<String, dynamic> json) => PlexMediaVersionDto(
|
||||
id: _flexibleIntOrZero(json['id']),
|
||||
videoResolution: readStringField(json, 'videoResolution') as String?,
|
||||
videoCodec: readStringField(json, 'videoCodec') as String?,
|
||||
bitrate: flexibleInt(json['bitrate']),
|
||||
width: flexibleInt(json['width']),
|
||||
height: flexibleInt(json['height']),
|
||||
container: readStringField(json, 'container') as String?,
|
||||
partKey: _readPartKey(json, 'partKey') as String,
|
||||
accessible: _readPartAccessible(json, 'accessible') as bool?,
|
||||
exists: _readPartExists(json, 'exists') as bool?,
|
||||
);
|
||||
PlexMediaVersionDto _$PlexMediaVersionDtoFromJson(Map<String, dynamic> json) =>
|
||||
PlexMediaVersionDto(
|
||||
id: _flexibleIntOrZero(json['id']),
|
||||
videoResolution: readStringField(json, 'videoResolution') as String?,
|
||||
videoCodec: readStringField(json, 'videoCodec') as String?,
|
||||
bitrate: flexibleInt(json['bitrate']),
|
||||
width: flexibleInt(json['width']),
|
||||
height: flexibleInt(json['height']),
|
||||
container: readStringField(json, 'container') as String?,
|
||||
partKey: _readPartKey(json, 'partKey') as String,
|
||||
accessible: _readPartAccessible(json, 'accessible') as bool?,
|
||||
exists: _readPartExists(json, 'exists') as bool?,
|
||||
);
|
||||
|
||||
PlexLibraryDto _$PlexLibraryDtoFromJson(Map<String, dynamic> json) => PlexLibraryDto(
|
||||
key: readStringField(json, 'key') as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
type: json['type'] as String? ?? '',
|
||||
agent: json['agent'] as String?,
|
||||
scanner: json['scanner'] as String?,
|
||||
language: json['language'] as String?,
|
||||
uuid: json['uuid'] as String?,
|
||||
updatedAt: flexibleInt(json['updatedAt']),
|
||||
createdAt: flexibleInt(json['createdAt']),
|
||||
hidden: flexibleInt(json['hidden']),
|
||||
);
|
||||
PlexLibraryDto _$PlexLibraryDtoFromJson(Map<String, dynamic> json) =>
|
||||
PlexLibraryDto(
|
||||
key: readStringField(json, 'key') as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
type: json['type'] as String? ?? '',
|
||||
agent: json['agent'] as String?,
|
||||
scanner: json['scanner'] as String?,
|
||||
language: json['language'] as String?,
|
||||
uuid: json['uuid'] as String?,
|
||||
updatedAt: flexibleInt(json['updatedAt']),
|
||||
createdAt: flexibleInt(json['createdAt']),
|
||||
hidden: flexibleInt(json['hidden']),
|
||||
);
|
||||
|
||||
PlexPlaylistDto _$PlexPlaylistDtoFromJson(Map<String, dynamic> json) => PlexPlaylistDto(
|
||||
ratingKey: readStringField(json, 'ratingKey') as String? ?? '',
|
||||
key: json['key'] as String? ?? '',
|
||||
type: json['type'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
summary: json['summary'] as String?,
|
||||
smart: json['smart'] as bool? ?? false,
|
||||
playlistType: json['playlistType'] as String? ?? '',
|
||||
duration: flexibleInt(json['duration']),
|
||||
leafCount: flexibleInt(json['leafCount']),
|
||||
composite: json['composite'] as String?,
|
||||
addedAt: flexibleInt(json['addedAt']),
|
||||
updatedAt: flexibleInt(json['updatedAt']),
|
||||
lastViewedAt: flexibleInt(json['lastViewedAt']),
|
||||
viewCount: flexibleInt(json['viewCount']),
|
||||
content: json['content'] as String?,
|
||||
guid: json['guid'] as String?,
|
||||
thumb: json['thumb'] as String?,
|
||||
);
|
||||
PlexPlaylistDto _$PlexPlaylistDtoFromJson(Map<String, dynamic> json) =>
|
||||
PlexPlaylistDto(
|
||||
ratingKey: readStringField(json, 'ratingKey') as String? ?? '',
|
||||
key: json['key'] as String? ?? '',
|
||||
type: json['type'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
summary: json['summary'] as String?,
|
||||
smart: json['smart'] as bool? ?? false,
|
||||
playlistType: json['playlistType'] as String? ?? '',
|
||||
duration: flexibleInt(json['duration']),
|
||||
leafCount: flexibleInt(json['leafCount']),
|
||||
composite: json['composite'] as String?,
|
||||
addedAt: flexibleInt(json['addedAt']),
|
||||
updatedAt: flexibleInt(json['updatedAt']),
|
||||
lastViewedAt: flexibleInt(json['lastViewedAt']),
|
||||
viewCount: flexibleInt(json['viewCount']),
|
||||
content: json['content'] as String?,
|
||||
guid: json['guid'] as String?,
|
||||
thumb: json['thumb'] as String?,
|
||||
);
|
||||
|
||||
PlexHubDto _$PlexHubDtoFromJson(Map<String, dynamic> json) => PlexHubDto(
|
||||
hubKey: readStringField(json, 'key') as String? ?? '',
|
||||
@@ -72,125 +75,130 @@ PlexHubDto _$PlexHubDtoFromJson(Map<String, dynamic> json) => PlexHubDto(
|
||||
items: _hubItemsFromJson(_readHubItems(json, 'items')),
|
||||
);
|
||||
|
||||
PlexMetadataDto _$PlexMetadataDtoFromJson(Map<String, dynamic> json) => PlexMetadataDto(
|
||||
ratingKey: _readMetadataRatingKey(json, 'ratingKey') as String? ?? '',
|
||||
key: json['key'] as String?,
|
||||
guid: json['guid'] as String?,
|
||||
studio: json['studio'] as String?,
|
||||
type: json['type'] as String?,
|
||||
title: json['title'] as String?,
|
||||
titleSort: json['titleSort'] as String?,
|
||||
contentRating: json['contentRating'] as String?,
|
||||
summary: json['summary'] as String?,
|
||||
rating: (json['rating'] as num?)?.toDouble(),
|
||||
audienceRating: (json['audienceRating'] as num?)?.toDouble(),
|
||||
userRating: (json['userRating'] as num?)?.toDouble(),
|
||||
year: flexibleInt(json['year']),
|
||||
originallyAvailableAt: json['originallyAvailableAt'] as String?,
|
||||
thumb: json['thumb'] as String?,
|
||||
art: json['art'] as String?,
|
||||
duration: flexibleInt(json['duration']),
|
||||
addedAt: flexibleInt(json['addedAt']),
|
||||
updatedAt: flexibleInt(json['updatedAt']),
|
||||
lastViewedAt: flexibleInt(json['lastViewedAt']),
|
||||
grandparentTitle: json['grandparentTitle'] as String?,
|
||||
grandparentThumb: json['grandparentThumb'] as String?,
|
||||
grandparentArt: json['grandparentArt'] as String?,
|
||||
grandparentRatingKey: readStringField(json, 'grandparentRatingKey') as String?,
|
||||
parentTitle: json['parentTitle'] as String?,
|
||||
parentThumb: json['parentThumb'] as String?,
|
||||
parentRatingKey: readStringField(json, 'parentRatingKey') as String?,
|
||||
parentIndex: flexibleInt(json['parentIndex']),
|
||||
index: flexibleInt(json['index']),
|
||||
grandparentTheme: json['grandparentTheme'] as String?,
|
||||
viewOffset: flexibleInt(json['viewOffset']),
|
||||
viewCount: flexibleInt(json['viewCount']),
|
||||
leafCount: flexibleInt(json['leafCount']),
|
||||
viewedLeafCount: flexibleInt(json['viewedLeafCount']),
|
||||
childCount: flexibleInt(json['childCount']),
|
||||
role: (json['Role'] as List<dynamic>?)?.map((e) => PlexRoleDto.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
mediaVersions: (json['Media'] as List<dynamic>?)
|
||||
?.map((e) => PlexMediaVersionDto.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
genre: _tagListFromJson(json['Genre']),
|
||||
director: _tagListFromJson(json['Director']),
|
||||
writer: _tagListFromJson(json['Writer']),
|
||||
producer: _tagListFromJson(json['Producer']),
|
||||
country: _tagListFromJson(json['Country']),
|
||||
collection: _tagListFromJson(json['Collection']),
|
||||
label: _tagListFromJson(json['Label']),
|
||||
style: _tagListFromJson(json['Style']),
|
||||
mood: _tagListFromJson(json['Mood']),
|
||||
audioLanguage: json['audioLanguage'] as String?,
|
||||
subtitleLanguage: json['subtitleLanguage'] as String?,
|
||||
subtitleMode: flexibleInt(json['subtitleMode']),
|
||||
playlistItemID: flexibleInt(json['playlistItemID']),
|
||||
playQueueItemID: flexibleInt(json['playQueueItemID']),
|
||||
librarySectionID: flexibleInt(json['librarySectionID']),
|
||||
librarySectionTitle: json['librarySectionTitle'] as String?,
|
||||
ratingImage: json['ratingImage'] as String?,
|
||||
audienceRatingImage: json['audienceRatingImage'] as String?,
|
||||
tagline: json['tagline'] as String?,
|
||||
originalTitle: json['originalTitle'] as String?,
|
||||
editionTitle: json['editionTitle'] as String?,
|
||||
subtype: json['subtype'] as String?,
|
||||
extraType: flexibleInt(json['extraType']),
|
||||
primaryExtraKey: json['primaryExtraKey'] as String?,
|
||||
clearLogo: json['clearLogo'] as String?,
|
||||
backgroundSquare: json['backgroundSquare'] as String?,
|
||||
);
|
||||
PlexMetadataDto _$PlexMetadataDtoFromJson(Map<String, dynamic> json) =>
|
||||
PlexMetadataDto(
|
||||
ratingKey: _readMetadataRatingKey(json, 'ratingKey') as String? ?? '',
|
||||
key: json['key'] as String?,
|
||||
guid: json['guid'] as String?,
|
||||
studio: json['studio'] as String?,
|
||||
type: json['type'] as String?,
|
||||
title: json['title'] as String?,
|
||||
titleSort: json['titleSort'] as String?,
|
||||
contentRating: json['contentRating'] as String?,
|
||||
summary: json['summary'] as String?,
|
||||
rating: (json['rating'] as num?)?.toDouble(),
|
||||
audienceRating: (json['audienceRating'] as num?)?.toDouble(),
|
||||
userRating: (json['userRating'] as num?)?.toDouble(),
|
||||
year: flexibleInt(json['year']),
|
||||
originallyAvailableAt: json['originallyAvailableAt'] as String?,
|
||||
thumb: json['thumb'] as String?,
|
||||
art: json['art'] as String?,
|
||||
duration: flexibleInt(json['duration']),
|
||||
addedAt: flexibleInt(json['addedAt']),
|
||||
updatedAt: flexibleInt(json['updatedAt']),
|
||||
lastViewedAt: flexibleInt(json['lastViewedAt']),
|
||||
grandparentTitle: json['grandparentTitle'] as String?,
|
||||
grandparentThumb: json['grandparentThumb'] as String?,
|
||||
grandparentArt: json['grandparentArt'] as String?,
|
||||
grandparentRatingKey:
|
||||
readStringField(json, 'grandparentRatingKey') as String?,
|
||||
parentTitle: json['parentTitle'] as String?,
|
||||
parentThumb: json['parentThumb'] as String?,
|
||||
parentRatingKey: readStringField(json, 'parentRatingKey') as String?,
|
||||
parentIndex: flexibleInt(json['parentIndex']),
|
||||
index: flexibleInt(json['index']),
|
||||
grandparentTheme: json['grandparentTheme'] as String?,
|
||||
viewOffset: flexibleInt(json['viewOffset']),
|
||||
viewCount: flexibleInt(json['viewCount']),
|
||||
leafCount: flexibleInt(json['leafCount']),
|
||||
viewedLeafCount: flexibleInt(json['viewedLeafCount']),
|
||||
childCount: flexibleInt(json['childCount']),
|
||||
role: (json['Role'] as List<dynamic>?)
|
||||
?.map((e) => PlexRoleDto.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
mediaVersions: (json['Media'] as List<dynamic>?)
|
||||
?.map((e) => PlexMediaVersionDto.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
genre: _tagListFromJson(json['Genre']),
|
||||
director: _tagListFromJson(json['Director']),
|
||||
writer: _tagListFromJson(json['Writer']),
|
||||
producer: _tagListFromJson(json['Producer']),
|
||||
country: _tagListFromJson(json['Country']),
|
||||
collection: _tagListFromJson(json['Collection']),
|
||||
label: _tagListFromJson(json['Label']),
|
||||
style: _tagListFromJson(json['Style']),
|
||||
mood: _tagListFromJson(json['Mood']),
|
||||
audioLanguage: json['audioLanguage'] as String?,
|
||||
subtitleLanguage: json['subtitleLanguage'] as String?,
|
||||
subtitleMode: flexibleInt(json['subtitleMode']),
|
||||
playlistItemID: flexibleInt(json['playlistItemID']),
|
||||
playQueueItemID: flexibleInt(json['playQueueItemID']),
|
||||
librarySectionID: flexibleInt(json['librarySectionID']),
|
||||
librarySectionTitle: json['librarySectionTitle'] as String?,
|
||||
ratingImage: json['ratingImage'] as String?,
|
||||
audienceRatingImage: json['audienceRatingImage'] as String?,
|
||||
tagline: json['tagline'] as String?,
|
||||
originalTitle: json['originalTitle'] as String?,
|
||||
editionTitle: json['editionTitle'] as String?,
|
||||
subtype: json['subtype'] as String?,
|
||||
extraType: flexibleInt(json['extraType']),
|
||||
primaryExtraKey: json['primaryExtraKey'] as String?,
|
||||
clearLogo: json['clearLogo'] as String?,
|
||||
backgroundSquare: json['backgroundSquare'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlexMetadataDtoToJson(PlexMetadataDto instance) => <String, dynamic>{
|
||||
'ratingKey': instance.ratingKey,
|
||||
'key': ?instance.key,
|
||||
'guid': ?instance.guid,
|
||||
'studio': ?instance.studio,
|
||||
'type': ?instance.type,
|
||||
'title': ?instance.title,
|
||||
'titleSort': ?instance.titleSort,
|
||||
'contentRating': ?instance.contentRating,
|
||||
'summary': ?instance.summary,
|
||||
'rating': ?instance.rating,
|
||||
'audienceRating': ?instance.audienceRating,
|
||||
'userRating': ?instance.userRating,
|
||||
'year': ?instance.year,
|
||||
'originallyAvailableAt': ?instance.originallyAvailableAt,
|
||||
'thumb': ?instance.thumb,
|
||||
'art': ?instance.art,
|
||||
'duration': ?instance.duration,
|
||||
'addedAt': ?instance.addedAt,
|
||||
'updatedAt': ?instance.updatedAt,
|
||||
'lastViewedAt': ?instance.lastViewedAt,
|
||||
'grandparentTitle': ?instance.grandparentTitle,
|
||||
'grandparentThumb': ?instance.grandparentThumb,
|
||||
'grandparentArt': ?instance.grandparentArt,
|
||||
'grandparentRatingKey': ?instance.grandparentRatingKey,
|
||||
'parentTitle': ?instance.parentTitle,
|
||||
'parentThumb': ?instance.parentThumb,
|
||||
'parentRatingKey': ?instance.parentRatingKey,
|
||||
'parentIndex': ?instance.parentIndex,
|
||||
'index': ?instance.index,
|
||||
'grandparentTheme': ?instance.grandparentTheme,
|
||||
'viewOffset': ?instance.viewOffset,
|
||||
'viewCount': ?instance.viewCount,
|
||||
'leafCount': ?instance.leafCount,
|
||||
'viewedLeafCount': ?instance.viewedLeafCount,
|
||||
'childCount': ?instance.childCount,
|
||||
'audioLanguage': ?instance.audioLanguage,
|
||||
'subtitleLanguage': ?instance.subtitleLanguage,
|
||||
'subtitleMode': ?instance.subtitleMode,
|
||||
'playlistItemID': ?instance.playlistItemID,
|
||||
'playQueueItemID': ?instance.playQueueItemID,
|
||||
'librarySectionID': ?instance.librarySectionID,
|
||||
'librarySectionTitle': ?instance.librarySectionTitle,
|
||||
'ratingImage': ?instance.ratingImage,
|
||||
'audienceRatingImage': ?instance.audienceRatingImage,
|
||||
'tagline': ?instance.tagline,
|
||||
'originalTitle': ?instance.originalTitle,
|
||||
'editionTitle': ?instance.editionTitle,
|
||||
'subtype': ?instance.subtype,
|
||||
'extraType': ?instance.extraType,
|
||||
'primaryExtraKey': ?instance.primaryExtraKey,
|
||||
'clearLogo': ?instance.clearLogo,
|
||||
'backgroundSquare': ?instance.backgroundSquare,
|
||||
};
|
||||
Map<String, dynamic> _$PlexMetadataDtoToJson(PlexMetadataDto instance) =>
|
||||
<String, dynamic>{
|
||||
'ratingKey': instance.ratingKey,
|
||||
'key': ?instance.key,
|
||||
'guid': ?instance.guid,
|
||||
'studio': ?instance.studio,
|
||||
'type': ?instance.type,
|
||||
'title': ?instance.title,
|
||||
'titleSort': ?instance.titleSort,
|
||||
'contentRating': ?instance.contentRating,
|
||||
'summary': ?instance.summary,
|
||||
'rating': ?instance.rating,
|
||||
'audienceRating': ?instance.audienceRating,
|
||||
'userRating': ?instance.userRating,
|
||||
'year': ?instance.year,
|
||||
'originallyAvailableAt': ?instance.originallyAvailableAt,
|
||||
'thumb': ?instance.thumb,
|
||||
'art': ?instance.art,
|
||||
'duration': ?instance.duration,
|
||||
'addedAt': ?instance.addedAt,
|
||||
'updatedAt': ?instance.updatedAt,
|
||||
'lastViewedAt': ?instance.lastViewedAt,
|
||||
'grandparentTitle': ?instance.grandparentTitle,
|
||||
'grandparentThumb': ?instance.grandparentThumb,
|
||||
'grandparentArt': ?instance.grandparentArt,
|
||||
'grandparentRatingKey': ?instance.grandparentRatingKey,
|
||||
'parentTitle': ?instance.parentTitle,
|
||||
'parentThumb': ?instance.parentThumb,
|
||||
'parentRatingKey': ?instance.parentRatingKey,
|
||||
'parentIndex': ?instance.parentIndex,
|
||||
'index': ?instance.index,
|
||||
'grandparentTheme': ?instance.grandparentTheme,
|
||||
'viewOffset': ?instance.viewOffset,
|
||||
'viewCount': ?instance.viewCount,
|
||||
'leafCount': ?instance.leafCount,
|
||||
'viewedLeafCount': ?instance.viewedLeafCount,
|
||||
'childCount': ?instance.childCount,
|
||||
'audioLanguage': ?instance.audioLanguage,
|
||||
'subtitleLanguage': ?instance.subtitleLanguage,
|
||||
'subtitleMode': ?instance.subtitleMode,
|
||||
'playlistItemID': ?instance.playlistItemID,
|
||||
'playQueueItemID': ?instance.playQueueItemID,
|
||||
'librarySectionID': ?instance.librarySectionID,
|
||||
'librarySectionTitle': ?instance.librarySectionTitle,
|
||||
'ratingImage': ?instance.ratingImage,
|
||||
'audienceRatingImage': ?instance.audienceRatingImage,
|
||||
'tagline': ?instance.tagline,
|
||||
'originalTitle': ?instance.originalTitle,
|
||||
'editionTitle': ?instance.editionTitle,
|
||||
'subtype': ?instance.subtype,
|
||||
'extraType': ?instance.extraType,
|
||||
'primaryExtraKey': ?instance.primaryExtraKey,
|
||||
'clearLogo': ?instance.clearLogo,
|
||||
'backgroundSquare': ?instance.backgroundSquare,
|
||||
};
|
||||
|
||||
@@ -6,16 +6,18 @@ part of 'anilist_session.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AnilistSession _$AnilistSessionFromJson(Map<String, dynamic> json) => AnilistSession(
|
||||
accessToken: json['access_token'] as String,
|
||||
expiresAt: (json['expires_at'] as num).toInt(),
|
||||
createdAt: (json['created_at'] as num).toInt(),
|
||||
username: json['username'] as String?,
|
||||
);
|
||||
AnilistSession _$AnilistSessionFromJson(Map<String, dynamic> json) =>
|
||||
AnilistSession(
|
||||
accessToken: json['access_token'] as String,
|
||||
expiresAt: (json['expires_at'] as num).toInt(),
|
||||
createdAt: (json['created_at'] as num).toInt(),
|
||||
username: json['username'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AnilistSessionToJson(AnilistSession instance) => <String, dynamic>{
|
||||
'access_token': instance.accessToken,
|
||||
'expires_at': instance.expiresAt,
|
||||
'username': instance.username,
|
||||
'created_at': instance.createdAt,
|
||||
};
|
||||
Map<String, dynamic> _$AnilistSessionToJson(AnilistSession instance) =>
|
||||
<String, dynamic>{
|
||||
'access_token': instance.accessToken,
|
||||
'expires_at': instance.expiresAt,
|
||||
'username': instance.username,
|
||||
'created_at': instance.createdAt,
|
||||
};
|
||||
|
||||
@@ -14,10 +14,11 @@ MalSession _$MalSessionFromJson(Map<String, dynamic> json) => MalSession(
|
||||
username: json['username'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MalSessionToJson(MalSession instance) => <String, dynamic>{
|
||||
'access_token': instance.accessToken,
|
||||
'refresh_token': instance.refreshToken,
|
||||
'expires_at': instance.expiresAt,
|
||||
'username': instance.username,
|
||||
'created_at': instance.createdAt,
|
||||
};
|
||||
Map<String, dynamic> _$MalSessionToJson(MalSession instance) =>
|
||||
<String, dynamic>{
|
||||
'access_token': instance.accessToken,
|
||||
'refresh_token': instance.refreshToken,
|
||||
'expires_at': instance.expiresAt,
|
||||
'username': instance.username,
|
||||
'created_at': instance.createdAt,
|
||||
};
|
||||
|
||||
@@ -12,8 +12,9 @@ SimklSession _$SimklSessionFromJson(Map<String, dynamic> json) => SimklSession(
|
||||
username: json['username'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SimklSessionToJson(SimklSession instance) => <String, dynamic>{
|
||||
'access_token': instance.accessToken,
|
||||
'username': instance.username,
|
||||
'created_at': instance.createdAt,
|
||||
};
|
||||
Map<String, dynamic> _$SimklSessionToJson(SimklSession instance) =>
|
||||
<String, dynamic>{
|
||||
'access_token': instance.accessToken,
|
||||
'username': instance.username,
|
||||
'created_at': instance.createdAt,
|
||||
};
|
||||
|
||||
@@ -15,11 +15,12 @@ TraktSession _$TraktSessionFromJson(Map<String, dynamic> json) => TraktSession(
|
||||
username: json['username'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TraktSessionToJson(TraktSession instance) => <String, dynamic>{
|
||||
'access_token': instance.accessToken,
|
||||
'refresh_token': instance.refreshToken,
|
||||
'expires_at': instance.expiresAt,
|
||||
'username': instance.username,
|
||||
'scope': instance.scope,
|
||||
'created_at': instance.createdAt,
|
||||
};
|
||||
Map<String, dynamic> _$TraktSessionToJson(TraktSession instance) =>
|
||||
<String, dynamic>{
|
||||
'access_token': instance.accessToken,
|
||||
'refresh_token': instance.refreshToken,
|
||||
'expires_at': instance.expiresAt,
|
||||
'username': instance.username,
|
||||
'scope': instance.scope,
|
||||
'created_at': instance.createdAt,
|
||||
};
|
||||
|
||||
@@ -13,9 +13,10 @@ RecentRoom _$RecentRoomFromJson(Map<String, dynamic> json) => RecentRoom(
|
||||
controlMode: _controlModeFromIndex((json['controlMode'] as num?)?.toInt()),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RecentRoomToJson(RecentRoom instance) => <String, dynamic>{
|
||||
'code': instance.code,
|
||||
'name': ?instance.name,
|
||||
'lastUsed': _dateTimeToMillis(instance.lastUsed),
|
||||
'controlMode': ?_controlModeToIndex(instance.controlMode),
|
||||
};
|
||||
Map<String, dynamic> _$RecentRoomToJson(RecentRoom instance) =>
|
||||
<String, dynamic>{
|
||||
'code': instance.code,
|
||||
'name': ?instance.name,
|
||||
'lastUsed': _dateTimeToMillis(instance.lastUsed),
|
||||
'controlMode': ?_controlModeToIndex(instance.controlMode),
|
||||
};
|
||||
|
||||
@@ -482,6 +482,22 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: f23ea33b3863f119b58ed1b586e881a46bd28715ddcc4dbc33104524e3434131
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.5"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -70,6 +70,7 @@ dependencies:
|
||||
cronet_http: ^1.6.0
|
||||
win_http: ^0.2.0
|
||||
collection: ^1.18.0
|
||||
freezed_annotation: ^3.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -83,6 +84,7 @@ dev_dependencies:
|
||||
shared_preferences_platform_interface: ^2.4.0
|
||||
path_provider_platform_interface: ^2.1.0
|
||||
plugin_platform_interface: ^2.1.0
|
||||
freezed: ^3.2.5
|
||||
|
||||
dependency_overrides:
|
||||
auto_updater_platform_interface:
|
||||
|
||||
@@ -2,6 +2,9 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_part.dart';
|
||||
import 'package:plezy/media/media_role.dart';
|
||||
import 'package:plezy/media/media_version.dart';
|
||||
|
||||
/// Backend-agnostic [MediaItem] tests. Existing coverage is split between
|
||||
/// `plex_mappers_test` and `jellyfin_mappers_test` — those exercise the
|
||||
@@ -187,6 +190,137 @@ void main() {
|
||||
expect(original.copyWith(title: 'New').backend, backend, reason: 'copyWith must preserve backend');
|
||||
}
|
||||
});
|
||||
|
||||
test('preserves Plex-only fields when omitted', () {
|
||||
const original = PlexMediaItem(
|
||||
id: 'p1',
|
||||
kind: MediaKind.movie,
|
||||
title: 'Old',
|
||||
editionTitle: 'Director Cut',
|
||||
audienceRating: 8.9,
|
||||
ratingImage: 'rottentomatoes://rating',
|
||||
audienceRatingImage: 'rottentomatoes://audience',
|
||||
subtitleLanguage: 'eng',
|
||||
subtitleMode: 1,
|
||||
trailerKey: '/library/metadata/1',
|
||||
playlistItemId: 42,
|
||||
playQueueItemId: 7,
|
||||
subtype: 'trailer',
|
||||
extraType: 1,
|
||||
);
|
||||
|
||||
final copy = original.copyWith(title: 'New');
|
||||
|
||||
expect(copy.title, 'New');
|
||||
expect(copy.editionTitle, 'Director Cut');
|
||||
expect(copy.audienceRating, 8.9);
|
||||
expect(copy.ratingImage, 'rottentomatoes://rating');
|
||||
expect(copy.audienceRatingImage, 'rottentomatoes://audience');
|
||||
expect(copy.subtitleLanguage, 'eng');
|
||||
expect(copy.subtitleMode, 1);
|
||||
expect(copy.trailerKey, '/library/metadata/1');
|
||||
expect(copy.playlistItemId, 42);
|
||||
expect(copy.playQueueItemId, 7);
|
||||
expect(copy.subtype, 'trailer');
|
||||
expect(copy.extraType, 1);
|
||||
});
|
||||
|
||||
test('preserves Jellyfin playlist item id when omitted', () {
|
||||
const original = JellyfinMediaItem(
|
||||
id: 'j1',
|
||||
kind: MediaKind.movie,
|
||||
title: 'Old',
|
||||
playlistItemId: 'playlist-entry-1',
|
||||
);
|
||||
|
||||
final copy = original.copyWith(title: 'New');
|
||||
|
||||
expect(copy.title, 'New');
|
||||
expect(copy.playlistItemId, 'playlist-entry-1');
|
||||
});
|
||||
|
||||
test('can clear nullable fields explicitly', () {
|
||||
final original = _movie(title: 'Movie', viewCount: 1, durationMs: 1000, viewOffsetMs: 500);
|
||||
|
||||
final copy = original.copyWith(title: null, viewOffsetMs: null);
|
||||
|
||||
expect(copy.title, isNull);
|
||||
expect(copy.viewOffsetMs, isNull);
|
||||
expect(copy.viewCount, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('MediaItem JSON', () {
|
||||
test('round-trips Plex-only fields', () {
|
||||
const original = PlexMediaItem(
|
||||
id: 'p1',
|
||||
kind: MediaKind.movie,
|
||||
title: 'Movie',
|
||||
editionTitle: 'Theatrical',
|
||||
audienceRating: 9.1,
|
||||
ratingImage: 'rottentomatoes://rating',
|
||||
audienceRatingImage: 'rottentomatoes://audience',
|
||||
genres: ['Drama'],
|
||||
roles: [MediaRole(id: '1', tag: 'Actor', role: 'Lead', thumbPath: '/photo')],
|
||||
mediaVersions: [
|
||||
MediaVersion(
|
||||
id: 'v1',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
parts: [MediaPart(id: 'part1', streamPath: '/stream', sizeBytes: 1000)],
|
||||
),
|
||||
],
|
||||
subtitleLanguage: 'eng',
|
||||
subtitleMode: 2,
|
||||
trailerKey: '/trailer',
|
||||
playlistItemId: 4,
|
||||
playQueueItemId: 5,
|
||||
subtype: 'trailer',
|
||||
extraType: 9,
|
||||
);
|
||||
|
||||
final json = original.toJson();
|
||||
final decoded = MediaItem.fromJson(json);
|
||||
|
||||
expect(json['backend'], 'plex');
|
||||
expect(json.containsKey('summary'), isFalse);
|
||||
expect(decoded, isA<PlexMediaItem>());
|
||||
final plex = decoded as PlexMediaItem;
|
||||
expect(plex.editionTitle, 'Theatrical');
|
||||
expect(plex.audienceRating, 9.1);
|
||||
expect(plex.ratingImage, 'rottentomatoes://rating');
|
||||
expect(plex.audienceRatingImage, 'rottentomatoes://audience');
|
||||
expect(plex.genres, ['Drama']);
|
||||
expect(plex.roles?.single.tag, 'Actor');
|
||||
expect(plex.mediaVersions?.single.parts.single.streamPath, '/stream');
|
||||
expect(plex.subtitleLanguage, 'eng');
|
||||
expect(plex.subtitleMode, 2);
|
||||
expect(plex.trailerKey, '/trailer');
|
||||
expect(plex.playlistItemId, 4);
|
||||
expect(plex.playQueueItemId, 5);
|
||||
expect(plex.subtype, 'trailer');
|
||||
expect(plex.extraType, 9);
|
||||
});
|
||||
|
||||
test('round-trips Jellyfin playlist item id', () {
|
||||
const original = JellyfinMediaItem(id: 'j1', kind: MediaKind.movie, title: 'Movie', playlistItemId: 'entry-1');
|
||||
|
||||
final json = original.toJson();
|
||||
final decoded = MediaItem.fromJson(json);
|
||||
|
||||
expect(json['backend'], 'jellyfin');
|
||||
expect(decoded, isA<JellyfinMediaItem>());
|
||||
expect((decoded as JellyfinMediaItem).playlistItemId, 'entry-1');
|
||||
});
|
||||
|
||||
test('missing backend keeps legacy Plex fallback', () {
|
||||
final decoded = MediaItem.fromJson({'id': 'legacy', 'kind': 'movie'});
|
||||
|
||||
expect(decoded, isA<PlexMediaItem>());
|
||||
expect(decoded.backend, MediaBackend.plex);
|
||||
expect(decoded.id, 'legacy');
|
||||
expect(decoded.kind, MediaKind.movie);
|
||||
});
|
||||
});
|
||||
|
||||
group('MediaItem.displayTitle', () {
|
||||
|
||||
Reference in New Issue
Block a user