Initial commit

This commit is contained in:
admin1
2026-07-14 11:11:36 +08:00
commit 656499cf94
604 changed files with 119518 additions and 0 deletions
@@ -0,0 +1,164 @@
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;
}