723 lines
21 KiB
Dart
723 lines
21 KiB
Dart
|
|
|
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
|
|
part 'library.freezed.dart';
|
|
part 'library.g.dart';
|
|
|
|
@freezed
|
|
abstract class EmbyRawUserData with _$EmbyRawUserData {
|
|
const factory EmbyRawUserData({
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false)
|
|
int? PlaybackPositionTicks,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? PlayCount,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? UnplayedItemCount,
|
|
@JsonKey(includeIfNull: false) bool? IsFavorite,
|
|
@JsonKey(includeIfNull: false) bool? Played,
|
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
@Default(<String, dynamic>{})
|
|
Map<String, dynamic> extra,
|
|
}) = _EmbyRawUserData;
|
|
|
|
factory EmbyRawUserData.fromJson(Map<String, dynamic> json) =>
|
|
_$EmbyRawUserDataFromJson(json).copyWith(extra: json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class EmbyRawItem with _$EmbyRawItem {
|
|
const factory EmbyRawItem({
|
|
required String Id,
|
|
required String Name,
|
|
@JsonKey(includeIfNull: false) String? Type,
|
|
@JsonKey(includeIfNull: false) String? Overview,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? ProductionYear,
|
|
@JsonKey(fromJson: _doubleOrNull, includeIfNull: false)
|
|
double? CommunityRating,
|
|
@JsonKey(includeIfNull: false) String? PremiereDate,
|
|
@JsonKey(includeIfNull: false) String? SeriesName,
|
|
@JsonKey(includeIfNull: false) String? SeriesId,
|
|
@JsonKey(includeIfNull: false) String? SeasonId,
|
|
@JsonKey(includeIfNull: false) String? SeasonName,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? IndexNumber,
|
|
@JsonKey(includeIfNull: false) String? ParentId,
|
|
@JsonKey(includeIfNull: false) String? PrimaryImageTag,
|
|
@JsonKey(fromJson: _stringMapOrNull, includeIfNull: false)
|
|
Map<String, String>? ImageTags,
|
|
@JsonKey(fromJson: _stringListOrNull, includeIfNull: false)
|
|
List<String>? BackdropImageTags,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? RunTimeTicks,
|
|
@JsonKey(includeIfNull: false) EmbyRawUserData? UserData,
|
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
@Default(<String, dynamic>{})
|
|
Map<String, dynamic> extra,
|
|
}) = _EmbyRawItem;
|
|
|
|
factory EmbyRawItem.fromJson(Map<String, dynamic> json) =>
|
|
_$EmbyRawItemFromJson(json).copyWith(extra: json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class EmbyRawSeason with _$EmbyRawSeason {
|
|
const factory EmbyRawSeason({
|
|
required String Id,
|
|
required String Name,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? IndexNumber,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? ProductionYear,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? ChildCount,
|
|
@JsonKey(includeIfNull: false) String? PrimaryImageTag,
|
|
@JsonKey(fromJson: _stringMapOrNull, includeIfNull: false)
|
|
Map<String, String>? ImageTags,
|
|
}) = _EmbyRawSeason;
|
|
|
|
factory EmbyRawSeason.fromJson(Map<String, dynamic> json) =>
|
|
_$EmbyRawSeasonFromJson(json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class EmbyRawLibrary with _$EmbyRawLibrary {
|
|
const factory EmbyRawLibrary({
|
|
required String Id,
|
|
required String Name,
|
|
@JsonKey(includeIfNull: false) String? CollectionType,
|
|
@JsonKey(fromJson: _stringMapOrNull, includeIfNull: false)
|
|
Map<String, String>? ImageTags,
|
|
}) = _EmbyRawLibrary;
|
|
|
|
factory EmbyRawLibrary.fromJson(Map<String, dynamic> json) =>
|
|
_$EmbyRawLibraryFromJson(json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class EmbyRawLibraries with _$EmbyRawLibraries {
|
|
const factory EmbyRawLibraries({required List<EmbyRawLibrary> Items}) =
|
|
_EmbyRawLibraries;
|
|
|
|
factory EmbyRawLibraries.fromJson(Map<String, dynamic> json) =>
|
|
_$EmbyRawLibrariesFromJson(json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class EmbyRawMediaStream with _$EmbyRawMediaStream {
|
|
const factory EmbyRawMediaStream({
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Index,
|
|
@JsonKey(includeIfNull: false) String? Type,
|
|
@JsonKey(includeIfNull: false) String? Language,
|
|
@JsonKey(includeIfNull: false) String? Codec,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Channels,
|
|
@JsonKey(includeIfNull: false) String? DisplayTitle,
|
|
@JsonKey(includeIfNull: false) String? Title,
|
|
@JsonKey(includeIfNull: false) bool? IsDefault,
|
|
@JsonKey(includeIfNull: false) bool? IsForced,
|
|
@JsonKey(includeIfNull: false) bool? IsExternal,
|
|
@JsonKey(includeIfNull: false) String? DeliveryMethod,
|
|
@JsonKey(includeIfNull: false) String? DeliveryUrl,
|
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
@Default(<String, dynamic>{})
|
|
Map<String, dynamic> extra,
|
|
}) = _EmbyRawMediaStream;
|
|
|
|
factory EmbyRawMediaStream.fromJson(Map<String, dynamic> json) =>
|
|
_$EmbyRawMediaStreamFromJson(json).copyWith(extra: json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class EmbyRawMediaSource with _$EmbyRawMediaSource {
|
|
const factory EmbyRawMediaSource({
|
|
@JsonKey(includeIfNull: false) String? Id,
|
|
@JsonKey(includeIfNull: false) String? Name,
|
|
@JsonKey(includeIfNull: false) String? Container,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Bitrate,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Size,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Width,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Height,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false)
|
|
int? DefaultSubtitleStreamIndex,
|
|
@JsonKey(fromJson: _intOrNull, includeIfNull: false)
|
|
int? DefaultAudioStreamIndex,
|
|
@JsonKey(includeIfNull: false) List<EmbyRawMediaStream>? MediaStreams,
|
|
}) = _EmbyRawMediaSource;
|
|
|
|
factory EmbyRawMediaSource.fromJson(Map<String, dynamic> json) =>
|
|
_$EmbyRawMediaSourceFromJson(json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class LibraryLatestReq with _$LibraryLatestReq {
|
|
const LibraryLatestReq._();
|
|
const factory LibraryLatestReq({required String parentId, int? limit}) =
|
|
_LibraryLatestReq;
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'parentId': parentId,
|
|
if (limit != null) 'limit': limit,
|
|
};
|
|
}
|
|
|
|
@freezed
|
|
abstract class LibraryLatestRes with _$LibraryLatestRes {
|
|
const factory LibraryLatestRes({
|
|
required String parentId,
|
|
required List<EmbyRawItem> items,
|
|
@Default(0) int totalCount,
|
|
}) = _LibraryLatestRes;
|
|
|
|
factory LibraryLatestRes.fromJson(Map<String, dynamic> json) =>
|
|
_$LibraryLatestResFromJson(json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class LibraryResumeRes with _$LibraryResumeRes {
|
|
const factory LibraryResumeRes({required List<EmbyRawItem> Items}) =
|
|
_LibraryResumeRes;
|
|
|
|
factory LibraryResumeRes.fromJson(Map<String, dynamic> json) =>
|
|
_$LibraryResumeResFromJson(json);
|
|
}
|
|
|
|
typedef LibraryViewsRes = EmbyRawLibraries;
|
|
|
|
@freezed
|
|
abstract class MediaDetailReq with _$MediaDetailReq {
|
|
const MediaDetailReq._();
|
|
const factory MediaDetailReq({required String itemId}) = _MediaDetailReq;
|
|
|
|
Map<String, dynamic> toJson() => {'itemId': itemId};
|
|
}
|
|
|
|
@freezed
|
|
abstract class EmbyRawMediaDetailSeriesExtra
|
|
with _$EmbyRawMediaDetailSeriesExtra {
|
|
const factory EmbyRawMediaDetailSeriesExtra({
|
|
required List<EmbyRawItem> nextUp,
|
|
required List<EmbyRawSeason> seasons,
|
|
}) = _EmbyRawMediaDetailSeriesExtra;
|
|
}
|
|
|
|
@freezed
|
|
abstract class EmbyRawMediaDetailSeriesContext
|
|
with _$EmbyRawMediaDetailSeriesContext {
|
|
const factory EmbyRawMediaDetailSeriesContext({
|
|
required EmbyRawItem seriesBase,
|
|
required EmbyRawMediaDetailSeriesExtra seriesExtra,
|
|
}) = _EmbyRawMediaDetailSeriesContext;
|
|
}
|
|
|
|
@freezed
|
|
abstract class MediaDetailRes with _$MediaDetailRes {
|
|
const factory MediaDetailRes({
|
|
required EmbyRawItem base,
|
|
EmbyRawMediaDetailSeriesExtra? seriesExtra,
|
|
EmbyRawMediaDetailSeriesContext? seriesContext,
|
|
List<EmbyRawItem>? similarItems,
|
|
List<EmbyRawItem>? boxSetItems,
|
|
}) = _MediaDetailRes;
|
|
}
|
|
|
|
@freezed
|
|
abstract class FavoriteSetReq with _$FavoriteSetReq {
|
|
const FavoriteSetReq._();
|
|
const factory FavoriteSetReq({
|
|
required String itemId,
|
|
required bool isFavorite,
|
|
}) = _FavoriteSetReq;
|
|
|
|
Map<String, dynamic> toJson() => {'itemId': itemId, 'isFavorite': isFavorite};
|
|
}
|
|
|
|
@freezed
|
|
abstract class FavoriteSetRes with _$FavoriteSetRes {
|
|
const factory FavoriteSetRes({
|
|
required String itemId,
|
|
required bool isFavorite,
|
|
}) = _FavoriteSetRes;
|
|
|
|
factory FavoriteSetRes.fromJson(Map<String, dynamic> json) =>
|
|
_$FavoriteSetResFromJson(json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class PlayedSetReq with _$PlayedSetReq {
|
|
const PlayedSetReq._();
|
|
const factory PlayedSetReq({required String itemId, required bool played}) =
|
|
_PlayedSetReq;
|
|
|
|
Map<String, dynamic> toJson() => {'itemId': itemId, 'played': played};
|
|
}
|
|
|
|
@freezed
|
|
abstract class PlayedSetRes with _$PlayedSetRes {
|
|
const factory PlayedSetRes({required String itemId, required bool played}) =
|
|
_PlayedSetRes;
|
|
|
|
factory PlayedSetRes.fromJson(Map<String, dynamic> json) =>
|
|
_$PlayedSetResFromJson(json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class HideFromResumeReq with _$HideFromResumeReq {
|
|
const HideFromResumeReq._();
|
|
const factory HideFromResumeReq({
|
|
required String itemId,
|
|
required bool hide,
|
|
}) = _HideFromResumeReq;
|
|
|
|
Map<String, dynamic> toJson() => {'itemId': itemId, 'hide': hide};
|
|
}
|
|
|
|
@freezed
|
|
abstract class HideFromResumeRes with _$HideFromResumeRes {
|
|
const factory HideFromResumeRes({
|
|
required String itemId,
|
|
required bool hide,
|
|
}) = _HideFromResumeRes;
|
|
|
|
factory HideFromResumeRes.fromJson(Map<String, dynamic> json) =>
|
|
_$HideFromResumeResFromJson(json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class MediaEpisodesReq with _$MediaEpisodesReq {
|
|
const MediaEpisodesReq._();
|
|
const factory MediaEpisodesReq({
|
|
required String seriesId,
|
|
required String seasonId,
|
|
}) = _MediaEpisodesReq;
|
|
|
|
Map<String, dynamic> toJson() => {'seriesId': seriesId, 'seasonId': seasonId};
|
|
}
|
|
|
|
@freezed
|
|
abstract class MediaEpisodesRes with _$MediaEpisodesRes {
|
|
const factory MediaEpisodesRes({
|
|
required String seriesId,
|
|
required String seasonId,
|
|
required List<EmbyRawItem> items,
|
|
}) = _MediaEpisodesRes;
|
|
}
|
|
|
|
@freezed
|
|
abstract class SeriesSeasonsReq with _$SeriesSeasonsReq {
|
|
const SeriesSeasonsReq._();
|
|
const factory SeriesSeasonsReq({required String seriesId}) =
|
|
_SeriesSeasonsReq;
|
|
|
|
Map<String, dynamic> toJson() => {'seriesId': seriesId};
|
|
}
|
|
|
|
@freezed
|
|
abstract class SeriesSeasonsRes with _$SeriesSeasonsRes {
|
|
const factory SeriesSeasonsRes({
|
|
required String seriesId,
|
|
required List<EmbyRawSeason> items,
|
|
}) = _SeriesSeasonsRes;
|
|
}
|
|
|
|
@freezed
|
|
abstract class SimilarItemsReq with _$SimilarItemsReq {
|
|
const SimilarItemsReq._();
|
|
const factory SimilarItemsReq({required String itemId, int? limit}) =
|
|
_SimilarItemsReq;
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'itemId': itemId,
|
|
if (limit != null) 'limit': limit,
|
|
};
|
|
}
|
|
|
|
@freezed
|
|
abstract class SimilarItemsRes with _$SimilarItemsRes {
|
|
const factory SimilarItemsRes({
|
|
required String itemId,
|
|
required List<EmbyRawItem> items,
|
|
}) = _SimilarItemsRes;
|
|
}
|
|
|
|
@freezed
|
|
abstract class AdditionalPartsReq with _$AdditionalPartsReq {
|
|
const factory AdditionalPartsReq({required String itemId}) =
|
|
_AdditionalPartsReq;
|
|
}
|
|
|
|
@freezed
|
|
abstract class AdditionalPartsRes with _$AdditionalPartsRes {
|
|
const factory AdditionalPartsRes({
|
|
required String itemId,
|
|
required List<EmbyRawItem> items,
|
|
}) = _AdditionalPartsRes;
|
|
}
|
|
|
|
@freezed
|
|
abstract class CrossServerSourceCard with _$CrossServerSourceCard {
|
|
const factory CrossServerSourceCard({
|
|
required String serverId,
|
|
required String serverName,
|
|
required String serverUrl,
|
|
required String token,
|
|
required String userId,
|
|
required String landingItemId,
|
|
required String mediaSourceId,
|
|
required EmbyRawItem item,
|
|
required EmbyRawMediaSource mediaSource,
|
|
MediaQualityInfo? quality,
|
|
}) = _CrossServerSourceCard;
|
|
}
|
|
|
|
@freezed
|
|
abstract class MediaQualityInfo with _$MediaQualityInfo {
|
|
const MediaQualityInfo._();
|
|
const factory MediaQualityInfo({
|
|
int? height,
|
|
int? width,
|
|
String? videoCodec,
|
|
String? container,
|
|
String? videoRange,
|
|
String? videoRangeType,
|
|
int? bitDepth,
|
|
double? fps,
|
|
}) = _MediaQualityInfo;
|
|
|
|
factory MediaQualityInfo.fromMediaSource(EmbyRawMediaSource source) {
|
|
final streams = source.MediaStreams ?? const [];
|
|
for (final stream in streams) {
|
|
if (stream.Type != 'Video') continue;
|
|
final ex = stream.extra;
|
|
final realFps = ex['RealFrameRate'] as num?;
|
|
final avgFps = ex['AverageFrameRate'] as num?;
|
|
return MediaQualityInfo(
|
|
height: (ex['Height'] as num?)?.toInt() ?? source.Height,
|
|
width: (ex['Width'] as num?)?.toInt() ?? source.Width,
|
|
videoCodec: stream.Codec,
|
|
container: source.Container,
|
|
videoRange: ex['VideoRange'] as String?,
|
|
videoRangeType: ex['VideoRangeType'] as String?,
|
|
bitDepth: (ex['BitDepth'] as num?)?.toInt(),
|
|
fps: realFps?.toDouble() ?? avgFps?.toDouble(),
|
|
);
|
|
}
|
|
final nameFallback = fromSourceName(
|
|
source.Name,
|
|
fallbackHeight: source.Height,
|
|
container: source.Container,
|
|
);
|
|
if (nameFallback != null) return nameFallback;
|
|
return MediaQualityInfo(
|
|
height: source.Height,
|
|
width: source.Width,
|
|
container: source.Container,
|
|
);
|
|
}
|
|
|
|
|
|
static MediaQualityInfo? fromSourceName(
|
|
String? name, {
|
|
int? fallbackHeight,
|
|
String? container,
|
|
}) {
|
|
final height = _heightFromName(name) ?? fallbackHeight;
|
|
final range = _rangeFromName(name);
|
|
if (height == null && range == null) return null;
|
|
return MediaQualityInfo(
|
|
height: height,
|
|
container: container,
|
|
videoRange: range,
|
|
);
|
|
}
|
|
|
|
static int? _heightFromName(String? name) {
|
|
if (name == null || name.isEmpty) return null;
|
|
final lower = name.toLowerCase();
|
|
if (RegExp(r'(?:^|[^0-9])2160(?![0-9])p?|\b4k\b|\buhd\b').hasMatch(lower)) {
|
|
return 2160;
|
|
}
|
|
if (RegExp(r'(?:^|[^0-9])1440(?![0-9])p?|\b2k\b').hasMatch(lower)) {
|
|
return 1440;
|
|
}
|
|
if (RegExp(r'(?:^|[^0-9])1080(?![0-9])p?|\bfhd\b').hasMatch(lower)) {
|
|
return 1080;
|
|
}
|
|
if (RegExp(r'(?:^|[^0-9])720(?![0-9])p?|\bhd\b').hasMatch(lower)) {
|
|
return 720;
|
|
}
|
|
if (RegExp(r'(?:^|[^0-9])480(?![0-9])p?').hasMatch(lower)) return 480;
|
|
return null;
|
|
}
|
|
|
|
static String? _rangeFromName(String? name) {
|
|
if (name == null || name.isEmpty) return null;
|
|
final lower = name.toLowerCase();
|
|
if (RegExp(r'\bdv\b|dolby[\s._-]*vision|dovi').hasMatch(lower)) {
|
|
return 'DolbyVision';
|
|
}
|
|
if (RegExp(r'hdr10\+|hdr10plus').hasMatch(lower)) return 'HDR10Plus';
|
|
if (RegExp(r'\bhlg\b').hasMatch(lower)) return 'HLG';
|
|
if (RegExp(r'hdr10').hasMatch(lower)) return 'HDR10';
|
|
if (RegExp(r'\bhdr\b').hasMatch(lower)) return 'HDR';
|
|
return null;
|
|
}
|
|
|
|
String get resolutionLabel {
|
|
final h = height;
|
|
if (h == null) return '';
|
|
if (h >= 2160) return '4K';
|
|
if (h >= 1440) return '2K';
|
|
if (h >= 1080) return '1080P';
|
|
if (h >= 720) return '720P';
|
|
return '${h}P';
|
|
}
|
|
|
|
String get dynamicRangeLabel {
|
|
final typeKnown = _canonicalDynamicRange(videoRangeType);
|
|
if (typeKnown != null) return typeKnown;
|
|
final rangeKnown = _canonicalDynamicRange(videoRange);
|
|
if (rangeKnown != null) return rangeKnown;
|
|
final typeRaw = _rawDynamicRange(videoRangeType);
|
|
if (typeRaw != null) return typeRaw;
|
|
final rangeRaw = _rawDynamicRange(videoRange);
|
|
if (rangeRaw != null) return rangeRaw;
|
|
return 'SDR';
|
|
}
|
|
|
|
|
|
static String? _canonicalDynamicRange(String? raw) {
|
|
if (raw == null) return null;
|
|
final compact = raw.replaceAll(' ', '');
|
|
if (compact.isEmpty) return null;
|
|
final upper = compact.toUpperCase();
|
|
if (upper.contains('DOVI') || upper.contains('DOLBYVISION')) {
|
|
return 'Dolby Vision';
|
|
}
|
|
if (upper == 'HDR10PLUS' || upper == 'HDR10+') return 'HDR10+';
|
|
if (upper == 'HDR10') return 'HDR10';
|
|
if (upper == 'HLG') return 'HLG';
|
|
if (upper == 'HDR') return 'HDR';
|
|
if (upper == 'SDR') return 'SDR';
|
|
return null;
|
|
}
|
|
|
|
|
|
static String? _rawDynamicRange(String? raw) {
|
|
if (raw == null) return null;
|
|
final trimmed = raw.trim();
|
|
return trimmed.isEmpty ? null : trimmed;
|
|
}
|
|
|
|
bool get isDolbyVision => dynamicRangeLabel == 'Dolby Vision';
|
|
bool get isHDR => dynamicRangeLabel != 'SDR';
|
|
|
|
String get codecLabel {
|
|
final c = videoCodec?.toLowerCase();
|
|
if (c == null || c.isEmpty) return '';
|
|
if (c == 'hevc' || c == 'h265') return 'H265';
|
|
if (c == 'h264' || c == 'avc') return 'H264';
|
|
if (c == 'av1') return 'AV1';
|
|
if (c == 'vp9') return 'VP9';
|
|
return c.toUpperCase();
|
|
}
|
|
|
|
String get bitDepthLabel {
|
|
if (bitDepth == null) return '';
|
|
return '${bitDepth}bit';
|
|
}
|
|
|
|
String get fpsLabel {
|
|
if (fps == null) return '';
|
|
final f = fps!;
|
|
if ((f - f.roundToDouble()).abs() < 0.1) return '${f.round()}fps';
|
|
return '${f.toStringAsFixed(1)}fps';
|
|
}
|
|
|
|
String get detailLine {
|
|
final parts = <String>[
|
|
codecLabel,
|
|
bitDepthLabel,
|
|
fpsLabel,
|
|
].where((s) => s.isNotEmpty).toList();
|
|
return parts.join(' ');
|
|
}
|
|
|
|
String get summary {
|
|
final parts = <String>[];
|
|
final res = resolutionLabel;
|
|
if (res.isNotEmpty) parts.add(res);
|
|
final dr = dynamicRangeLabel;
|
|
if (dr.isNotEmpty) parts.add(dr);
|
|
if (codecLabel.isNotEmpty) parts.add(codecLabel);
|
|
return parts.join(' · ');
|
|
}
|
|
}
|
|
|
|
@freezed
|
|
abstract class CrossServerSearchReq with _$CrossServerSearchReq {
|
|
const factory CrossServerSearchReq({
|
|
required String anchorType,
|
|
required String itemName,
|
|
required Map<String, String> providerIds,
|
|
Map<String, String>? seriesProviderIds,
|
|
int? parentIndexNumber,
|
|
int? indexNumber,
|
|
|
|
|
|
String? excludedServerId,
|
|
}) = _CrossServerSearchReq;
|
|
}
|
|
|
|
|
|
@freezed
|
|
abstract class TmdbAvailabilityReq with _$TmdbAvailabilityReq {
|
|
const factory TmdbAvailabilityReq({
|
|
required String tmdbId,
|
|
required String mediaType,
|
|
|
|
|
|
String? imdbId,
|
|
}) = _TmdbAvailabilityReq;
|
|
}
|
|
|
|
|
|
@freezed
|
|
abstract class TmdbLibraryHit with _$TmdbLibraryHit {
|
|
const factory TmdbLibraryHit({
|
|
required String serverId,
|
|
required String serverName,
|
|
required String itemId,
|
|
required String itemType,
|
|
required String name,
|
|
int? productionYear,
|
|
|
|
|
|
int? playbackPositionTicks,
|
|
|
|
|
|
int? runTimeTicks,
|
|
}) = _TmdbLibraryHit;
|
|
}
|
|
|
|
enum SortOrder {
|
|
ascending('Ascending'),
|
|
descending('Descending');
|
|
|
|
final String value;
|
|
const SortOrder(this.value);
|
|
|
|
static SortOrder? fromString(String? value) {
|
|
if (value == null) return null;
|
|
return SortOrder.values.firstWhere(
|
|
(e) => e.value == value,
|
|
orElse: () => SortOrder.ascending,
|
|
);
|
|
}
|
|
}
|
|
|
|
@freezed
|
|
abstract class LibraryItemsReq with _$LibraryItemsReq {
|
|
const factory LibraryItemsReq({
|
|
String? parentId,
|
|
String? includeItemTypes,
|
|
List<String>? genreIds,
|
|
String? searchTerm,
|
|
String? anyProviderIdEquals,
|
|
String? fields,
|
|
String? enableImageTypes,
|
|
bool? groupProgramsBySeries,
|
|
int? imageTypeLimit,
|
|
int? startIndex,
|
|
int? limit,
|
|
bool? recursive,
|
|
String? sortBy,
|
|
SortOrder? sortOrder,
|
|
}) = _LibraryItemsReq;
|
|
}
|
|
|
|
@freezed
|
|
abstract class LibraryItemsRes with _$LibraryItemsRes {
|
|
const factory LibraryItemsRes({
|
|
required String parentId,
|
|
required List<EmbyRawItem> items,
|
|
}) = _LibraryItemsRes;
|
|
}
|
|
|
|
@freezed
|
|
abstract class ItemCountsRes with _$ItemCountsRes {
|
|
const factory ItemCountsRes({
|
|
@JsonKey(name: 'MovieCount', fromJson: _intOrZero) required int movieCount,
|
|
@JsonKey(name: 'EpisodeCount', fromJson: _intOrZero)
|
|
required int episodeCount,
|
|
}) = _ItemCountsRes;
|
|
|
|
factory ItemCountsRes.fromJson(Map<String, dynamic> json) =>
|
|
_$ItemCountsResFromJson(json);
|
|
}
|
|
|
|
@freezed
|
|
abstract class GlobalSearchReq with _$GlobalSearchReq {
|
|
const factory GlobalSearchReq({
|
|
required String keyword,
|
|
String? includeItemTypes,
|
|
int? limitPerServer,
|
|
String? serverId,
|
|
}) = _GlobalSearchReq;
|
|
}
|
|
|
|
@freezed
|
|
abstract class GlobalSearchServerResult with _$GlobalSearchServerResult {
|
|
const factory GlobalSearchServerResult({
|
|
required String serverId,
|
|
required String serverName,
|
|
required String serverUrl,
|
|
required String token,
|
|
required String userId,
|
|
required List<EmbyRawItem> items,
|
|
}) = _GlobalSearchServerResult;
|
|
}
|
|
|
|
int? _intOrNull(Object? value) => (value as num?)?.toInt();
|
|
double? _doubleOrNull(Object? value) => (value as num?)?.toDouble();
|
|
int _intOrZero(Object? value) => (value as num?)?.toInt() ?? 0;
|
|
|
|
|
|
List<EmbyRawMediaSource> mediaSourcesOfItem(EmbyRawItem? item) {
|
|
final raw = item?.extra['MediaSources'];
|
|
if (raw is! List) return const [];
|
|
return raw
|
|
.whereType<Map<String, dynamic>>()
|
|
.map(EmbyRawMediaSource.fromJson)
|
|
.toList(growable: false);
|
|
}
|
|
|
|
|
|
Map<String, String> providerIdsOfItem(EmbyRawItem item) {
|
|
final raw = item.extra['ProviderIds'];
|
|
if (raw is! Map) return const {};
|
|
final ids = <String, String>{};
|
|
for (final entry in raw.entries) {
|
|
final key = entry.key?.toString() ?? '';
|
|
final value = entry.value?.toString() ?? '';
|
|
if (key.isNotEmpty && value.isNotEmpty) ids[key] = value;
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
Map<String, String> normalizeProviderIds(Map<String, String> raw) {
|
|
if (raw.isEmpty) return const {};
|
|
final out = <String, String>{};
|
|
for (final e in raw.entries) {
|
|
final k = e.key.trim().toLowerCase();
|
|
final v = e.value.trim();
|
|
if (k.isNotEmpty && v.isNotEmpty) out[k] = v;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
Map<String, String> normalizedProviderIdsOfItem(EmbyRawItem item) =>
|
|
normalizeProviderIds(providerIdsOfItem(item));
|
|
|
|
Map<String, String>? _stringMapOrNull(Object? value) {
|
|
if (value is! Map) return null;
|
|
return value.map((k, v) => MapEntry(k as String, v as String));
|
|
}
|
|
|
|
List<String>? _stringListOrNull(Object? value) {
|
|
if (value is! List) return null;
|
|
return value.cast<String>();
|
|
}
|