using System.Globalization; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Jellyfin; namespace ErsatzTV.Application.MediaCards; internal static class Mapper { internal static TelevisionShowCardViewModel ProjectToViewModel( ShowMetadata showMetadata, Option maybeJellyfin, Option maybeEmby) => new( showMetadata.ShowId, showMetadata.Title, showMetadata.Year?.ToString(CultureInfo.InvariantCulture), showMetadata.SortTitle, GetPoster(showMetadata, maybeJellyfin, maybeEmby), showMetadata.Show.State); internal static TelevisionSeasonCardViewModel ProjectToViewModel( Season season, Option maybeJellyfin, Option maybeEmby) => new( season.Show.ShowMetadata.HeadOrNone().Match(m => m.Title ?? string.Empty, () => string.Empty), season.Id, season.SeasonNumber, GetSeasonName(season.SeasonNumber), string.Empty, GetSeasonName(season.SeasonNumber), season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby)) .IfNone(string.Empty), season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString(CultureInfo.InvariantCulture), season.State); internal static TelevisionSeasonCardViewModel ProjectToViewModel( SeasonMetadata seasonMetadata, Option maybeJellyfin, Option maybeEmby) { string showTitle = seasonMetadata.Season.Show.ShowMetadata.HeadOrNone().Match( m => m.Title ?? string.Empty, () => string.Empty); return new TelevisionSeasonCardViewModel( showTitle, seasonMetadata.SeasonId, seasonMetadata.Season.SeasonNumber, showTitle, GetSeasonName(seasonMetadata.Season.SeasonNumber), $"{showTitle}_{seasonMetadata.Season.SeasonNumber:0000}", GetPoster(seasonMetadata, maybeJellyfin, maybeEmby), seasonMetadata.Season.SeasonNumber == 0 ? "S" : seasonMetadata.Season.SeasonNumber.ToString(CultureInfo.InvariantCulture), seasonMetadata.Season.State); } internal static TelevisionEpisodeCardViewModel ProjectToViewModel( EpisodeMetadata episodeMetadata, Option maybeJellyfin, Option maybeEmby, bool isSearchResult, string localPath) => new( episodeMetadata.EpisodeId, episodeMetadata.ReleaseDate ?? SystemTime.MinValueUtc, episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Match( m => m.Title ?? string.Empty, () => string.Empty), episodeMetadata.Episode.Season.ShowId, episodeMetadata.Episode.SeasonId, episodeMetadata.Episode.Season.SeasonNumber, episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(em => em.EpisodeNumber, () => 0), episodeMetadata.Title, episodeMetadata.SortTitle, episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match( em => em.Plot ?? string.Empty, () => string.Empty), isSearchResult ? GetEpisodePoster(episodeMetadata, maybeJellyfin, maybeEmby) : GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby), episodeMetadata.Directors.Map(d => d.Name).ToList(), episodeMetadata.Writers.Map(w => w.Name).ToList(), episodeMetadata.Episode.State, episodeMetadata.Episode.GetHeadVersion().MediaFiles.Head().Path, localPath); internal static MovieCardViewModel ProjectToViewModel( MovieMetadata movieMetadata, Option maybeJellyfin, Option maybeEmby) => new( movieMetadata.MovieId, movieMetadata.Title, movieMetadata.Year?.ToString(CultureInfo.InvariantCulture), movieMetadata.SortTitle, GetPoster(movieMetadata, maybeJellyfin, maybeEmby), movieMetadata.Movie.State); internal static MusicVideoCardViewModel ProjectToViewModel( MusicVideoMetadata musicVideoMetadata, string localPath) => new( musicVideoMetadata.MusicVideoId, musicVideoMetadata.Title, musicVideoMetadata.MusicVideo.Artist.ArtistMetadata.Head().Title, musicVideoMetadata.SortTitle, musicVideoMetadata.Plot, musicVideoMetadata.Album, GetThumbnail(musicVideoMetadata, None, None), musicVideoMetadata.MusicVideo.State, musicVideoMetadata.MusicVideo.GetHeadVersion().MediaFiles.Head().Path, localPath); internal static OtherVideoCardViewModel ProjectToViewModel(OtherVideoMetadata otherVideoMetadata) { string poster = GetThumbnail(otherVideoMetadata, None, None); if (string.IsNullOrWhiteSpace(poster)) { poster = GetPoster(otherVideoMetadata, None, None); } return new OtherVideoCardViewModel( otherVideoMetadata.OtherVideoId, otherVideoMetadata.Title, otherVideoMetadata.OriginalTitle, otherVideoMetadata.SortTitle, poster, otherVideoMetadata.OtherVideo.State); } internal static SongCardViewModel ProjectToViewModel(SongMetadata songMetadata) { string album = string.IsNullOrWhiteSpace(songMetadata.Album) ? "" : $" - {songMetadata.Album}"; return new SongCardViewModel( songMetadata.SongId, songMetadata.Title, string.Join(", ", songMetadata.Artists) + album, songMetadata.SortTitle, GetThumbnail(songMetadata, None, None), songMetadata.Song.State); } internal static ImageCardViewModel ProjectToViewModel(ImageMetadata imageMetadata) => new( imageMetadata.ImageId, imageMetadata.Title, imageMetadata.OriginalTitle, imageMetadata.SortTitle, string.Empty, // TODO: thumbnail? imageMetadata.Image.State); internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) => new( artistMetadata.ArtistId, artistMetadata.Title, artistMetadata.Disambiguation, artistMetadata.SortTitle, GetThumbnail(artistMetadata, None, None), artistMetadata.Artist.State); internal static CollectionCardResultsViewModel ProjectToViewModel( Collection collection, Option maybeJellyfin, Option maybeEmby) => new( collection.Name, collection.MediaItems.OfType().Map( m => ProjectToViewModel(m.MovieMetadata.Head(), maybeJellyfin, maybeEmby) with { CustomIndex = GetCustomIndex(collection, m.Id) }).ToList(), collection.MediaItems.OfType() .Map(s => ProjectToViewModel(s.ShowMetadata.Head(), maybeJellyfin, maybeEmby)) .ToList(), collection.MediaItems.OfType().Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)) .ToList(), // collection view doesn't use local paths collection.MediaItems.OfType() .Map( e => ProjectToViewModel( e.EpisodeMetadata.Head(), maybeJellyfin, maybeEmby, false, string.Empty)) .ToList(), collection.MediaItems.OfType().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(), // collection view doesn't use local paths collection.MediaItems.OfType() .Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head(), string.Empty)) .ToList(), collection.MediaItems.OfType().Map(ov => ProjectToViewModel(ov.OtherVideoMetadata.Head())) .ToList(), collection.MediaItems.OfType().Map(s => ProjectToViewModel(s.SongMetadata.Head())) .ToList(), collection.MediaItems.OfType().Map(i => ProjectToViewModel(i.ImageMetadata.Head())).ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder }; internal static ActorCardViewModel ProjectToViewModel( Actor actor, Option maybeJellyfin, Option maybeEmby) { string artwork = actor.Artwork?.Path ?? string.Empty; if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://", StringComparison.OrdinalIgnoreCase)) { artwork = JellyfinUrl.RelativeProxyForArtwork(artwork) .SetQueryParam("fillHeight", 440); } else if (maybeEmby.IsSome && artwork.StartsWith("emby://", StringComparison.OrdinalIgnoreCase)) { artwork = EmbyUrl.RelativeProxyForArtwork(artwork) .SetQueryParam("maxHeight", 440); } return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork, MediaItemState.Normal); } private static int GetCustomIndex(Collection collection, int mediaItemId) => Optional(collection.CollectionItems.Find(ci => ci.MediaItemId == mediaItemId)) .Map(ci => ci.CustomIndex ?? 0) .IfNone(0); private static string GetSeasonName(int number) => number == 0 ? "Specials" : $"Season {number}"; private static string GetEpisodePoster( EpisodeMetadata episodeMetadata, Option maybeJellyfin, Option maybeEmby) { Option maybeSeasonMetadata = episodeMetadata.Episode.Season.SeasonMetadata.HeadOrNone(); return maybeSeasonMetadata.Match( seasonMetadata => GetPoster(seasonMetadata, maybeJellyfin, maybeEmby), () => { Option maybeShowMetadata = episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone(); return maybeShowMetadata.Match( showMetadata => GetPoster(showMetadata, maybeJellyfin, maybeEmby), () => string.Empty); }); } private static string GetPoster( Metadata metadata, Option maybeJellyfin, Option maybeEmby) { string poster = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster)) .Match(a => a.Path, string.Empty); if (maybeJellyfin.IsSome && poster.StartsWith("jellyfin://", StringComparison.OrdinalIgnoreCase)) { poster = JellyfinUrl.RelativeProxyForArtwork(poster) .SetQueryParam("fillHeight", 440); } else if (maybeEmby.IsSome && poster.StartsWith("emby://", StringComparison.OrdinalIgnoreCase)) { poster = EmbyUrl.RelativeProxyForArtwork(poster) .SetQueryParam("maxHeight", 440); } return poster; } private static string GetThumbnail( Metadata metadata, Option maybeJellyfin, Option maybeEmby) { string thumb = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Thumbnail)) .Match(a => a.Path, string.Empty); if (maybeJellyfin.IsSome && thumb.StartsWith("jellyfin://", StringComparison.OrdinalIgnoreCase)) { thumb = JellyfinUrl.RelativeProxyForArtwork(thumb) .SetQueryParam("fillHeight", 220); } else if (maybeEmby.IsSome && thumb.StartsWith("emby://", StringComparison.OrdinalIgnoreCase)) { thumb = EmbyUrl.RelativeProxyForArtwork(thumb) .SetQueryParam("maxHeight", 220); } return thumb; } }