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 buildDetailMetaParts( EmbyRawItem item, { EmbyRawMediaSource? source, }) { final parts = []; 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 lowercaseProviderIds(Map ids) { return { for (final e in ids.entries) if (e.key.isNotEmpty && e.value.isNotEmpty) e.key.toLowerCase(): e.value, }; } List extractGenres(EmbyRawItem item) { final genresRaw = item.extra['Genres']; if (genresRaw is List) { return genresRaw.whereType().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; }