add jellyfin media source (#185)
* wip * start to add jellyfin tables to db * code cleanup * finish adding jellyfin media source * sync jellyfin libraries * display list of jellyfin libraries * toggle jellyfin library sync * edit jellyfin path replacements * noop jellyfin scanners * get jellyfin admin user id on startup * implement jellyfin disconnect * add jellyfin libraries to list; start to query jellyfin library items * code cleanup * start to project jellyfin movies * save new jellyfin movies to db * basic jellyfin movie update * load jellyfin actor artwork * load jellyfin movie poster and fan art * more jellyfin artwork fixes, sync audio streams * jellyfin playback sort of works * skip jellyfin movies that are inaccessible * use ffprobe for jellyfin movie statistics * code cleanup * store jellyfin operating system * more jellyfin movie updates * update jellyfin movie poster and fan art * add jellyfin tv types * sync jellyfin shows * sync jellyfin seasons * sync jellyfin episodes * remove missing jellyfin television items * delete empty jellyfin seasons and shows * fix jellyfin updates * fix indexing jellyfin movie and show languages
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Infrastructure.Jellyfin.Models;
|
||||
using Refit;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin
|
||||
{
|
||||
[Headers("Accept: application/json")]
|
||||
public interface IJellyfinApi
|
||||
{
|
||||
[Get("/System/Info")]
|
||||
public Task<JellyfinSystemInformationResponse> GetSystemInformation(
|
||||
[Header("X-Emby-Token")]
|
||||
string apiKey);
|
||||
|
||||
[Get("/Users")]
|
||||
public Task<List<JellyfinUserResponse>> GetUsers(
|
||||
[Header("X-Emby-Token")]
|
||||
string apiKey);
|
||||
|
||||
[Get("/Library/VirtualFolders")]
|
||||
public Task<List<JellyfinLibraryResponse>> GetLibraries(
|
||||
[Header("X-Emby-Token")]
|
||||
string apiKey);
|
||||
|
||||
[Get("/Items")]
|
||||
public Task<JellyfinLibraryItemsResponse> GetMovieLibraryItems(
|
||||
[Header("X-Emby-Token")]
|
||||
string apiKey,
|
||||
[Query]
|
||||
string userId,
|
||||
[Query]
|
||||
string parentId,
|
||||
[Query]
|
||||
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People",
|
||||
[Query]
|
||||
string includeItemTypes = "Movie");
|
||||
|
||||
[Get("/Items")]
|
||||
public Task<JellyfinLibraryItemsResponse> GetShowLibraryItems(
|
||||
[Header("X-Emby-Token")]
|
||||
string apiKey,
|
||||
[Query]
|
||||
string userId,
|
||||
[Query]
|
||||
string parentId,
|
||||
[Query]
|
||||
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People",
|
||||
[Query]
|
||||
string includeItemTypes = "Series");
|
||||
|
||||
[Get("/Items")]
|
||||
public Task<JellyfinLibraryItemsResponse> GetSeasonLibraryItems(
|
||||
[Header("X-Emby-Token")]
|
||||
string apiKey,
|
||||
[Query]
|
||||
string userId,
|
||||
[Query]
|
||||
string parentId,
|
||||
[Query]
|
||||
string fields = "Path,DateCreated,Etag,Taglines",
|
||||
[Query]
|
||||
string includeItemTypes = "Season");
|
||||
|
||||
[Get("/Items")]
|
||||
public Task<JellyfinLibraryItemsResponse> GetEpisodeLibraryItems(
|
||||
[Header("X-Emby-Token")]
|
||||
string apiKey,
|
||||
[Query]
|
||||
string userId,
|
||||
[Query]
|
||||
string parentId,
|
||||
[Query]
|
||||
string fields = "Path,DateCreated,Etag,Overview",
|
||||
[Query]
|
||||
string includeItemTypes = "Episode");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using LanguageExt;
|
||||
using Newtonsoft.Json;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin
|
||||
{
|
||||
public class JellyfinSecretStore : IJellyfinSecretStore
|
||||
{
|
||||
public Task<Unit> DeleteAll() => SaveSecrets(new JellyfinSecrets());
|
||||
|
||||
public Task<JellyfinSecrets> ReadSecrets() =>
|
||||
File.ReadAllTextAsync(FileSystemLayout.JellyfinSecretsPath)
|
||||
.Map(JsonConvert.DeserializeObject<JellyfinSecrets>)
|
||||
.Map(s => Optional(s).IfNone(new JellyfinSecrets()));
|
||||
|
||||
public Task<Unit> SaveSecrets(JellyfinSecrets jellyfinSecrets) =>
|
||||
Some(JsonConvert.SerializeObject(jellyfinSecrets)).Match(
|
||||
s => File.WriteAllTextAsync(FileSystemLayout.JellyfinSecretsPath, s).ToUnit(),
|
||||
Task.FromResult(Unit.Default));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||
{
|
||||
public class JellyfinImageTagsResponse
|
||||
{
|
||||
public string Primary { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||
{
|
||||
public class JellyfinLibraryItemResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Etag { get; set; }
|
||||
public string Path { get; set; }
|
||||
public DateTimeOffset DateCreated { get; set; }
|
||||
public long RunTimeTicks { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
public int ProductionYear { get; set; }
|
||||
public string PremiereDate { get; set; }
|
||||
public List<JellyfinMediaStreamResponse> MediaStreams { get; set; }
|
||||
public string LocationType { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public List<string> Taglines { get; set; }
|
||||
public List<JellyfinStudioResponse> Studios { get; set; }
|
||||
public List<JellyfinPersonResponse> People { get; set; }
|
||||
public JellyfinImageTagsResponse ImageTags { get; set; }
|
||||
public List<string> BackdropImageTags { get; set; }
|
||||
public int? IndexNumber { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||
{
|
||||
public class JellyfinLibraryItemsResponse
|
||||
{
|
||||
public List<JellyfinLibraryItemResponse> Items { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||
{
|
||||
public class JellyfinLibraryResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string CollectionType { get; set; }
|
||||
public string ItemId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||
{
|
||||
public class JellyfinMediaStreamResponse
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Codec { get; set; }
|
||||
public string Language { get; set; }
|
||||
public bool? IsInterlaced { get; set; }
|
||||
public int? Height { get; set; }
|
||||
public int? Width { get; set; }
|
||||
public int Index { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
public bool IsForced { get; set; }
|
||||
public string Profile { get; set; }
|
||||
public string AspectRatio { get; set; }
|
||||
public int? Channels { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||
{
|
||||
public class JellyfinPersonResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Role { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string PrimaryImageTag { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||
{
|
||||
public class JellyfinStudioResponse
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||
{
|
||||
public class JellyfinSystemInformationResponse
|
||||
{
|
||||
public string ServerName { get; set; }
|
||||
public string OperatingSystem { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||
{
|
||||
public class JellyfinUserPolicyResponse
|
||||
{
|
||||
public bool IsAdministrator { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||
{
|
||||
public class JellyfinUserResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
public JellyfinUserPolicyResponse Policy { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user