165 lines
4.7 KiB
Dart
165 lines
4.7 KiB
Dart
|
|
import '../../core/contracts/library.dart';
|
||
|
|
import '../../shared/utils/cross_server_search_key.dart';
|
||
|
|
import '../../shared/utils/emby_ticks.dart';
|
||
|
|
import '../../shared/utils/format_utils.dart';
|
||
|
|
import '../../shared/utils/tmdb_key_utils.dart';
|
||
|
|
|
||
|
|
|
||
|
|
class TmdbMediaRef {
|
||
|
|
final String id;
|
||
|
|
final String mediaType;
|
||
|
|
|
||
|
|
const TmdbMediaRef({required this.id, required this.mediaType});
|
||
|
|
}
|
||
|
|
|
||
|
|
List<String> buildDetailMetaParts(
|
||
|
|
EmbyRawItem item, {
|
||
|
|
EmbyRawMediaSource? source,
|
||
|
|
}) {
|
||
|
|
final parts = <String>[];
|
||
|
|
final seriesName = item.SeriesName;
|
||
|
|
if (seriesName != null && seriesName.isNotEmpty) {
|
||
|
|
parts.add(seriesName);
|
||
|
|
}
|
||
|
|
if (item.ProductionYear != null) {
|
||
|
|
parts.add('${item.ProductionYear}');
|
||
|
|
}
|
||
|
|
final episode = episodeLabel(item);
|
||
|
|
if (episode != null) {
|
||
|
|
parts.add(episode);
|
||
|
|
}
|
||
|
|
final runtime = item.RunTimeTicks;
|
||
|
|
if (runtime != null && runtime > 0) {
|
||
|
|
final label = formatRuntimeMinutes(runtime ~/ kTicksPerMinute);
|
||
|
|
if (label != null) parts.add(label);
|
||
|
|
}
|
||
|
|
if (source != null) {
|
||
|
|
final videoStream = findVideoStream(source);
|
||
|
|
if (videoStream != null) {
|
||
|
|
final h = videoStream.extra['Height'] as int? ?? source.Height;
|
||
|
|
final range = videoStream.extra['VideoRange'] as String?;
|
||
|
|
if (h != null) {
|
||
|
|
final label = heightToLabel(h);
|
||
|
|
parts.add(range != null && range.isNotEmpty ? '$label $range' : label);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
final size = source.Size;
|
||
|
|
if (size != null && size > 0) parts.add(formatFileSize(size));
|
||
|
|
}
|
||
|
|
return parts;
|
||
|
|
}
|
||
|
|
|
||
|
|
EmbyRawMediaStream? findVideoStream(EmbyRawMediaSource source) {
|
||
|
|
final streams = source.MediaStreams;
|
||
|
|
if (streams == null) return null;
|
||
|
|
for (final s in streams) {
|
||
|
|
if (s.Type == 'Video') return s;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
String heightToLabel(int h) {
|
||
|
|
if (h >= 2160) return '4K';
|
||
|
|
if (h >= 1440) return '2K';
|
||
|
|
if (h >= 1080) return '1080P';
|
||
|
|
if (h >= 720) return '720P';
|
||
|
|
return '${h}P';
|
||
|
|
}
|
||
|
|
|
||
|
|
String? episodeLabel(EmbyRawItem item) {
|
||
|
|
final seasonName = item.SeasonName;
|
||
|
|
final index = item.IndexNumber;
|
||
|
|
if (seasonName == null && index == null) return null;
|
||
|
|
if (seasonName != null && seasonName.isNotEmpty && index != null) {
|
||
|
|
return '$seasonName ${formatEpisodeNumber(index)}';
|
||
|
|
}
|
||
|
|
if (seasonName != null && seasonName.isNotEmpty) return seasonName;
|
||
|
|
return formatEpisodeNumber(index);
|
||
|
|
}
|
||
|
|
|
||
|
|
String detailNavTitle(EmbyRawItem item, EmbyRawItem? seriesItem) {
|
||
|
|
if (item.Type != 'Episode') return item.Name;
|
||
|
|
final seriesName = item.SeriesName;
|
||
|
|
if (seriesName != null && seriesName.isNotEmpty) return seriesName;
|
||
|
|
final loadedSeriesName = seriesItem?.Name;
|
||
|
|
if (loadedSeriesName != null && loadedSeriesName.isNotEmpty) {
|
||
|
|
return loadedSeriesName;
|
||
|
|
}
|
||
|
|
return item.Name;
|
||
|
|
}
|
||
|
|
|
||
|
|
String? seriesIdForEpisode(EmbyRawItem item, String routeItemId) {
|
||
|
|
if (item.Type != 'Episode') return null;
|
||
|
|
final seriesId = item.SeriesId;
|
||
|
|
if (seriesId != null && seriesId.isNotEmpty) return seriesId;
|
||
|
|
if (routeItemId.isNotEmpty && routeItemId != item.Id) return routeItemId;
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
Map<String, String> lowercaseProviderIds(Map<String, String> ids) {
|
||
|
|
return {
|
||
|
|
for (final e in ids.entries)
|
||
|
|
if (e.key.isNotEmpty && e.value.isNotEmpty) e.key.toLowerCase(): e.value,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
List<String> extractGenres(EmbyRawItem item) {
|
||
|
|
final genresRaw = item.extra['Genres'];
|
||
|
|
if (genresRaw is List) {
|
||
|
|
return genresRaw.whereType<String>().where((g) => g.isNotEmpty).toList();
|
||
|
|
}
|
||
|
|
return const [];
|
||
|
|
}
|
||
|
|
|
||
|
|
double? detailProgressPercent(EmbyRawItem item) {
|
||
|
|
final percent = playbackProgress(item) * 100;
|
||
|
|
return percent < 1 ? null : percent;
|
||
|
|
}
|
||
|
|
|
||
|
|
String tmdbIdFromDetailItem(EmbyRawItem item) => tmdbIdFromItem(item);
|
||
|
|
|
||
|
|
TmdbMediaRef? tmdbMediaRefFor(EmbyRawItem item, EmbyRawItem? tmdbSeriesItem) {
|
||
|
|
final String mediaType;
|
||
|
|
final String tmdbId;
|
||
|
|
switch (item.Type) {
|
||
|
|
case 'Movie':
|
||
|
|
mediaType = 'movie';
|
||
|
|
tmdbId = tmdbIdFromDetailItem(item);
|
||
|
|
case 'Series':
|
||
|
|
mediaType = 'tv';
|
||
|
|
tmdbId = tmdbIdFromDetailItem(item);
|
||
|
|
case 'Episode':
|
||
|
|
final seriesItem = tmdbSeriesItem;
|
||
|
|
if (seriesItem == null) return null;
|
||
|
|
mediaType = 'tv';
|
||
|
|
tmdbId = tmdbIdFromDetailItem(seriesItem);
|
||
|
|
default:
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (tmdbId.isEmpty) return null;
|
||
|
|
return TmdbMediaRef(id: tmdbId, mediaType: mediaType);
|
||
|
|
}
|
||
|
|
|
||
|
|
CrossServerSearchReq? buildCrossServerReqForDetail(
|
||
|
|
EmbyRawItem displayItem,
|
||
|
|
EmbyRawItem? seriesItem,
|
||
|
|
) {
|
||
|
|
final providerIds = providerIdsOfItem(displayItem);
|
||
|
|
if (displayItem.Type == 'Movie') {
|
||
|
|
if (providerIds.isEmpty && displayItem.Name.isEmpty) return null;
|
||
|
|
return CrossServerSearchReq(
|
||
|
|
anchorType: 'Movie',
|
||
|
|
itemName: displayItem.Name,
|
||
|
|
providerIds: providerIds,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (displayItem.Type == 'Episode') {
|
||
|
|
return buildEpisodeCrossServerReq(
|
||
|
|
episode: displayItem,
|
||
|
|
seriesItem: seriesItem,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|