Stream custom live channels using your own media
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

569 lines
21 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Infrastructure.Jellyfin.Models;
using LanguageExt;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Refit;
using static LanguageExt.Prelude;
namespace ErsatzTV.Infrastructure.Jellyfin
{
public class JellyfinApiClient : IJellyfinApiClient
{
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILogger<JellyfinApiClient> _logger;
private readonly IMemoryCache _memoryCache;
public JellyfinApiClient(
IMemoryCache memoryCache,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger<JellyfinApiClient> logger)
{
_memoryCache = memoryCache;
_fallbackMetadataProvider = fallbackMetadataProvider;
_logger = logger;
}
public async Task<Either<BaseError, JellyfinServerInformation>> GetServerInformation(
string address,
string apiKey)
{
try
{
IJellyfinApi service = RestService.For<IJellyfinApi>(address);
return await service.GetSystemInformation(apiKey)
.Map(response => new JellyfinServerInformation(response.ServerName, response.OperatingSystem));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting jellyfin server name");
return BaseError.New(ex.Message);
}
}
public async Task<Either<BaseError, List<JellyfinLibrary>>> GetLibraries(string address, string apiKey)
{
try
{
IJellyfinApi service = RestService.For<IJellyfinApi>(address);
List<JellyfinLibraryResponse> libraries = await service.GetLibraries(apiKey);
return libraries
.Map(Project)
.Somes()
.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting jellyfin libraries");
return BaseError.New(ex.Message);
}
}
public async Task<Either<BaseError, string>> GetAdminUserId(string address, string apiKey)
{
try
{
IJellyfinApi service = RestService.For<IJellyfinApi>(address);
List<JellyfinUserResponse> users = await service.GetUsers(apiKey);
Option<string> maybeUserId = users
.Filter(user => user.Policy.IsAdministrator)
.Map(user => user.Id)
.HeadOrNone();
return maybeUserId.ToEither(BaseError.New("Unable to locate jellyfin admin user"));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting jellyfin admin user id");
return BaseError.New(ex.Message);
}
}
public async Task<Either<BaseError, List<JellyfinMovie>>> GetMovieLibraryItems(
string address,
string apiKey,
int mediaSourceId,
string libraryId)
{
try
{
if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{mediaSourceId}", out string userId))
{
IJellyfinApi service = RestService.For<IJellyfinApi>(address);
JellyfinLibraryItemsResponse items = await service.GetMovieLibraryItems(apiKey, userId, libraryId);
return items.Items
.Map(ProjectToMovie)
.Somes()
.ToList();
}
return BaseError.New("Jellyfin admin user id is not available");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting jellyfin movie library items");
return BaseError.New(ex.Message);
}
}
public async Task<Either<BaseError, List<JellyfinShow>>> GetShowLibraryItems(
string address,
string apiKey,
int mediaSourceId,
string libraryId)
{
try
{
if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{mediaSourceId}", out string userId))
{
IJellyfinApi service = RestService.For<IJellyfinApi>(address);
JellyfinLibraryItemsResponse items = await service.GetShowLibraryItems(apiKey, userId, libraryId);
return items.Items
.Map(ProjectToShow)
.Somes()
.ToList();
}
return BaseError.New("Jellyfin admin user id is not available");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting jellyfin show library items");
return BaseError.New(ex.Message);
}
}
public async Task<Either<BaseError, List<JellyfinSeason>>> GetSeasonLibraryItems(
string address,
string apiKey,
int mediaSourceId,
string showId)
{
try
{
if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{mediaSourceId}", out string userId))
{
IJellyfinApi service = RestService.For<IJellyfinApi>(address);
JellyfinLibraryItemsResponse items = await service.GetSeasonLibraryItems(apiKey, userId, showId);
return items.Items
.Map(ProjectToSeason)
.Somes()
.ToList();
}
return BaseError.New("Jellyfin admin user id is not available");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting jellyfin show library items");
return BaseError.New(ex.Message);
}
}
public async Task<Either<BaseError, List<JellyfinEpisode>>> GetEpisodeLibraryItems(
string address,
string apiKey,
int mediaSourceId,
string seasonId)
{
try
{
if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{mediaSourceId}", out string userId))
{
IJellyfinApi service = RestService.For<IJellyfinApi>(address);
JellyfinLibraryItemsResponse items = await service.GetEpisodeLibraryItems(apiKey, userId, seasonId);
return items.Items
.Map(ProjectToEpisode)
.Somes()
.ToList();
}
return BaseError.New("Jellyfin admin user id is not available");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting jellyfin episode library items");
return BaseError.New(ex.Message);
}
}
private static Option<JellyfinLibrary> Project(JellyfinLibraryResponse response) =>
response.CollectionType?.ToLowerInvariant() switch
{
"tvshows" => new JellyfinLibrary
{
ItemId = response.ItemId,
Name = response.Name,
MediaKind = LibraryMediaKind.Shows,
ShouldSyncItems = false,
Paths = new List<LibraryPath> { new() { Path = $"jellyfin://{response.ItemId}" } }
},
"movies" => new JellyfinLibrary
{
ItemId = response.ItemId,
Name = response.Name,
MediaKind = LibraryMediaKind.Movies,
ShouldSyncItems = false,
Paths = new List<LibraryPath> { new() { Path = $"jellyfin://{response.ItemId}" } }
},
// TODO: ??? for music libraries
_ => None
};
private Option<JellyfinMovie> ProjectToMovie(JellyfinLibraryItemResponse item)
{
try
{
if (item.LocationType != "FileSystem")
{
return None;
}
var version = new MediaVersion
{
Name = "Main",
Duration = TimeSpan.FromTicks(item.RunTimeTicks),
DateAdded = item.DateCreated.UtcDateTime,
MediaFiles = new List<MediaFile>
{
new()
{
Path = item.Path
}
},
Streams = new List<MediaStream>()
};
MovieMetadata metadata = ProjectToMovieMetadata(item);
var movie = new JellyfinMovie
{
ItemId = item.Id,
Etag = item.Etag,
MediaVersions = new List<MediaVersion> { version },
MovieMetadata = new List<MovieMetadata> { metadata }
};
return movie;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error projecting Jellyfin movie");
return None;
}
}
private MovieMetadata ProjectToMovieMetadata(JellyfinLibraryItemResponse item)
{
DateTime dateAdded = item.DateCreated.UtcDateTime;
// DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(item.UpdatedAt).DateTime;
var metadata = new MovieMetadata
{
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,
Year = item.ProductionYear,
Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty),
DateAdded = dateAdded,
Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(),
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
Actors = Optional(item.People).Flatten().Map(r => ProjectToModel(r, dateAdded)).ToList(),
Artwork = new List<Artwork>()
};
// set order on actors
for (var i = 0; i < metadata.Actors.Count; i++)
{
metadata.Actors[i].Order = i;
}
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
{
metadata.ReleaseDate = releaseDate;
}
if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary))
{
var poster = new Artwork
{
ArtworkKind = ArtworkKind.Poster,
Path = $"jellyfin:///Items/{item.Id}/Images/Primary?tag={item.ImageTags.Primary}",
DateAdded = dateAdded
};
metadata.Artwork.Add(poster);
}
if (item.BackdropImageTags.Any())
{
var fanArt = new Artwork
{
ArtworkKind = ArtworkKind.FanArt,
Path = $"jellyfin:///Items/{item.Id}/Images/Backdrop?tag={item.BackdropImageTags.Head()}",
DateAdded = dateAdded
};
metadata.Artwork.Add(fanArt);
}
return metadata;
}
private Actor ProjectToModel(JellyfinPersonResponse person, DateTime dateAdded)
{
var actor = new Actor { Name = person.Name, Role = person.Role };
if (!string.IsNullOrWhiteSpace(person.Id) && !string.IsNullOrWhiteSpace(person.PrimaryImageTag))
{
actor.Artwork = new Artwork
{
Path = $"jellyfin:///Items/{person.Id}/Images/Primary?tag={person.PrimaryImageTag}",
ArtworkKind = ArtworkKind.Thumbnail,
DateAdded = dateAdded
};
}
return actor;
}
private Option<JellyfinShow> ProjectToShow(JellyfinLibraryItemResponse item)
{
try
{
if (item.LocationType != "FileSystem")
{
return None;
}
ShowMetadata metadata = ProjectToShowMetadata(item);
var show = new JellyfinShow
{
ItemId = item.Id,
Etag = item.Etag,
ShowMetadata = new List<ShowMetadata> { metadata }
};
return show;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error projecting Jellyfin show");
return None;
}
}
private ShowMetadata ProjectToShowMetadata(JellyfinLibraryItemResponse item)
{
DateTime dateAdded = item.DateCreated.UtcDateTime;
// DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(item.UpdatedAt).DateTime;
var metadata = new ShowMetadata
{
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,
Year = item.ProductionYear,
Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty),
DateAdded = dateAdded,
Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(),
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
Actors = Optional(item.People).Flatten().Map(r => ProjectToModel(r, dateAdded)).ToList(),
Artwork = new List<Artwork>()
};
// set order on actors
for (var i = 0; i < metadata.Actors.Count; i++)
{
metadata.Actors[i].Order = i;
}
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
{
metadata.ReleaseDate = releaseDate;
}
if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary))
{
var poster = new Artwork
{
ArtworkKind = ArtworkKind.Poster,
Path = $"jellyfin:///Items/{item.Id}/Images/Primary?tag={item.ImageTags.Primary}",
DateAdded = dateAdded
};
metadata.Artwork.Add(poster);
}
if (item.BackdropImageTags.Any())
{
var fanArt = new Artwork
{
ArtworkKind = ArtworkKind.FanArt,
Path = $"jellyfin:///Items/{item.Id}/Images/Backdrop?tag={item.BackdropImageTags.Head()}",
DateAdded = dateAdded
};
metadata.Artwork.Add(fanArt);
}
return metadata;
}
private Option<JellyfinSeason> ProjectToSeason(JellyfinLibraryItemResponse item)
{
try
{
if (item.LocationType != "FileSystem")
{
return None;
}
DateTime dateAdded = item.DateCreated.UtcDateTime;
// DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
var metadata = new SeasonMetadata
{
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Year = item.ProductionYear,
DateAdded = dateAdded,
Artwork = new List<Artwork>()
};
if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary))
{
var poster = new Artwork
{
ArtworkKind = ArtworkKind.Poster,
Path = $"jellyfin:///Items/{item.Id}/Images/Primary?tag={item.ImageTags.Primary}",
DateAdded = dateAdded
};
metadata.Artwork.Add(poster);
}
if (item.BackdropImageTags.Any())
{
var fanArt = new Artwork
{
ArtworkKind = ArtworkKind.FanArt,
Path = $"jellyfin:///Items/{item.Id}/Images/Backdrop?tag={item.BackdropImageTags.Head()}",
DateAdded = dateAdded
};
metadata.Artwork.Add(fanArt);
}
var season = new JellyfinSeason
{
ItemId = item.Id,
Etag = item.Etag,
SeasonMetadata = new List<SeasonMetadata> { metadata }
};
if (item.IndexNumber.HasValue)
{
season.SeasonNumber = item.IndexNumber.Value;
}
return season;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error projecting Jellyfin show");
return None;
}
}
private Option<JellyfinEpisode> ProjectToEpisode(JellyfinLibraryItemResponse item)
{
try
{
if (item.LocationType != "FileSystem")
{
return None;
}
var version = new MediaVersion
{
Name = "Main",
Duration = TimeSpan.FromTicks(item.RunTimeTicks),
DateAdded = item.DateCreated.UtcDateTime,
MediaFiles = new List<MediaFile>
{
new()
{
Path = item.Path
}
},
Streams = new List<MediaStream>()
};
EpisodeMetadata metadata = ProjectToEpisodeMetadata(item);
var episode = new JellyfinEpisode
{
ItemId = item.Id,
Etag = item.Etag,
MediaVersions = new List<MediaVersion> { version },
EpisodeMetadata = new List<EpisodeMetadata> { metadata }
};
if (item.IndexNumber.HasValue)
{
episode.EpisodeNumber = item.IndexNumber.Value;
}
return episode;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error projecting Jellyfin movie");
return None;
}
}
private EpisodeMetadata ProjectToEpisodeMetadata(JellyfinLibraryItemResponse item)
{
DateTime dateAdded = item.DateCreated.UtcDateTime;
// DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(item.UpdatedAt).DateTime;
var metadata = new EpisodeMetadata
{
Title = item.Name,
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Plot = item.Overview,
Year = item.ProductionYear,
DateAdded = dateAdded,
Genres = new List<Genre>(),
Tags = new List<Tag>(),
Studios = new List<Studio>(),
Actors = new List<Actor>(),
Artwork = new List<Artwork>()
};
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
{
metadata.ReleaseDate = releaseDate;
}
if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary))
{
var thumbnail = new Artwork
{
ArtworkKind = ArtworkKind.Thumbnail,
Path = $"jellyfin:///Items/{item.Id}/Images/Primary?tag={item.ImageTags.Primary}",
DateAdded = dateAdded
};
metadata.Artwork.Add(thumbnail);
}
return metadata;
}
}
}