diff --git a/CHANGELOG.md b/CHANGELOG.md index 15c702f0..f0ddd9cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Align default docker image (no acceleration) with new images from [ErsatzTV-ffmpeg](https://github.com/jasongdove/ErsatzTV-ffmpeg) ### Changed -- Plex libraries now retrieve all metadata and statistics from Plex; ffprobe is no longer used -- Plex libraries now direct stream content from Plex when files are not found on ErsatzTV's file system +- Plex, Jellyfin and Emby libraries now retrieve all metadata and statistics from the media server; ffprobe is no longer used +- Plex, Jellyfin and Emby libraries now direct stream content when files are not found on ErsatzTV's file system - Content will still be normalized according to the Channel and FFmpeg Profile settings - Streaming from disk is preferred, so every playback attempt will first check the local file system -- Jellyfin libraries will retrieve all metadata and statistics from Jellyfin when local files are unavailable -- Jellyfin libraries now direct stream content from Jellyfin when files are not found on ErsatzTV's file system - - Content will still be normalized according to the Channel and FFmpeg Profile settings - - Streaming from disk is preferred, so every playback attempt will first check the local file system ## [0.7.4-beta] - 2023-02-12 ### Added diff --git a/ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs b/ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs index c0beb3e4..36e60993 100644 --- a/ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs +++ b/ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs @@ -1,3 +1,3 @@ namespace ErsatzTV.Application.Emby; -public record EmbyConnectionParametersViewModel(string Address); +public record EmbyConnectionParametersViewModel(string Address, string ApiKey); diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs b/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs index 336de180..fb2832e5 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs @@ -1,5 +1,7 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Emby; +using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Repositories; using Microsoft.Extensions.Caching.Memory; @@ -9,14 +11,17 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler> { private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IEmbySecretStore _embySecretStore; private readonly IMemoryCache _memoryCache; public GetEmbyConnectionParametersHandler( IMemoryCache memoryCache, - IMediaSourceRepository mediaSourceRepository) + IMediaSourceRepository mediaSourceRepository, + IEmbySecretStore embySecretStore) { _memoryCache = memoryCache; _mediaSourceRepository = mediaSourceRepository; + _embySecretStore = embySecretStore; } public async Task> Handle( @@ -30,7 +35,7 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler maybeParameters = await Validate() - .MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address)) + .MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address, cp.ApiKey)) .Map(v => v.ToEither()); return maybeParameters.Match( @@ -44,7 +49,8 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler> Validate() => EmbyMediaSourceMustExist() - .BindT(MediaSourceMustHaveActiveConnection); + .BindT(MediaSourceMustHaveActiveConnection) + .BindT(MediaSourceMustHaveApiKey); private Task> EmbyMediaSourceMustExist() => _mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone()) @@ -59,8 +65,21 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler new ConnectionParameters(embyMediaSource, connection)) .ToValidation("Emby media source requires an active connection"); } + + private async Task> MediaSourceMustHaveApiKey( + ConnectionParameters connectionParameters) + { + EmbySecrets secrets = await _embySecretStore.ReadSecrets(); + return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) + .Where(match => match) + .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) + .ToValidation("Emby media source requires an api key"); + } private record ConnectionParameters( EmbyMediaSource EmbyMediaSource, - EmbyConnection ActiveConnection); + EmbyConnection ActiveConnection) + { + public string ApiKey { get; set; } + } } diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index 9e5dd2a8..08877955 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -496,6 +496,21 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< playoutItem, $"http://localhost:{Settings.ListenPort}/media/jellyfin/{itemId}"); } + + // attempt to remotely stream emby + Option embyItemId = playoutItem.MediaItem switch + { + EmbyEpisode e => e.ItemId, + EmbyMovie m => m.ItemId, + _ => None + }; + + foreach (string itemId in embyItemId) + { + return new PlayoutItemWithPath( + playoutItem, + $"http://localhost:{Settings.ListenPort}/media/emby/{itemId}"); + } return new PlayoutItemDoesNotExistOnDisk(path); } diff --git a/ErsatzTV.Core/Emby/EmbyMediaStreamType.cs b/ErsatzTV.Core/Emby/EmbyMediaStreamType.cs new file mode 100644 index 00000000..91434574 --- /dev/null +++ b/ErsatzTV.Core/Emby/EmbyMediaStreamType.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Core.Emby; + +public static class EmbyMediaStreamType +{ + public static readonly string Video = "Video"; + public static readonly string Audio = "Audio"; + public static readonly string Subtitle = "Subtitle"; +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs b/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs index 4ade1d13..bec6c14e 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs @@ -270,7 +270,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector _logger.LogDebug("Checking for JS Script at {Path}", jsScriptPath); if (!_localFileSystem.FileExists(jsScriptPath)) { - _logger.LogWarning("Unable to locate episode audio stream selector script; falling back to built-in logic"); + _logger.LogDebug("Unable to locate episode audio stream selector script; falling back to built-in logic"); return Option.None; } diff --git a/ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs b/ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs index a5320f95..f6a9574c 100644 --- a/ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs +++ b/ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs @@ -34,4 +34,10 @@ public interface IEmbyApiClient string apiKey, string parentId, string includeItemTypes); + + Task> GetPlaybackInfo( + string address, + string apiKey, + EmbyLibrary library, + string itemId); } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs index 687eeb90..e7f1a981 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs @@ -126,6 +126,8 @@ public class EmbyMovieRepository : IEmbyMovieRepository .ThenInclude(mv => mv.MediaFiles) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Streams) + .Include(m => m.MediaVersions) + .ThenInclude(mv => mv.Chapters) .Include(m => m.MovieMetadata) .ThenInclude(mm => mm.Genres) .Include(m => m.MovieMetadata) @@ -366,6 +368,7 @@ public class EmbyMovieRepository : IEmbyMovieRepository MediaVersion incomingVersion = incoming.MediaVersions.Head(); version.Name = incomingVersion.Name; version.DateAdded = incomingVersion.DateAdded; + version.Chapters = incomingVersion.Chapters; // media file MediaFile file = version.MediaFiles.Head(); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs index dfb2ca6b..2bdb7ef4 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs @@ -135,6 +135,8 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository .ThenInclude(mv => mv.MediaFiles) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Streams) + .Include(m => m.MediaVersions) + .ThenInclude(mv => mv.Chapters) .Include(m => m.EpisodeMetadata) .ThenInclude(mm => mm.Artwork) .Include(m => m.EpisodeMetadata) @@ -724,6 +726,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository MediaVersion incomingVersion = incoming.MediaVersions.Head(); version.Name = incomingVersion.Name; version.DateAdded = incomingVersion.DateAdded; + version.Chapters = incomingVersion.Chapters; // media file MediaFile file = version.MediaFiles.Head(); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs index e6dd6545..3fdc0038 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs @@ -729,6 +729,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository MediaVersion incomingVersion = incoming.MediaVersions.Head(); version.Name = incomingVersion.Name; version.DateAdded = incomingVersion.DateAdded; + version.Chapters = incomingVersion.Chapters; // media file MediaFile file = version.MediaFiles.Head(); diff --git a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs index c4931e0a..8d35c6d5 100644 --- a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs +++ b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Core; +using System.Globalization; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Interfaces.Emby; @@ -191,6 +192,26 @@ public class EmbyApiClient : IEmbyApiClient } } + public async Task> GetPlaybackInfo( + string address, + string apiKey, + EmbyLibrary library, + string itemId) + { + try + { + IEmbyApi service = RestService.For(address); + EmbyPlaybackInfoResponse playbackInfo = await service.GetPlaybackInfo(apiKey, itemId); + Option maybeVersion = ProjectToMediaVersion(playbackInfo); + return maybeVersion.ToEither(() => BaseError.New("Unable to locate Emby statistics")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting Emby playback info"); + return BaseError.New(ex.Message); + } + } + private static async IAsyncEnumerable GetPagedLibraryContents( string address, string apiKey, @@ -328,10 +349,11 @@ public class EmbyApiClient : IEmbyApiClient } } + var duration = TimeSpan.FromTicks(item.RunTimeTicks); var version = new MediaVersion { Name = "Main", - Duration = TimeSpan.FromTicks(item.RunTimeTicks), + Duration = duration, DateAdded = item.DateCreated.UtcDateTime, MediaFiles = new List { @@ -340,7 +362,8 @@ public class EmbyApiClient : IEmbyApiClient Path = path } }, - Streams = new List() + Streams = new List(), + Chapters = ProjectToModel(Optional(item.Chapters).Flatten(), duration) }; MovieMetadata metadata = ProjectToMovieMetadata(item); @@ -362,6 +385,29 @@ public class EmbyApiClient : IEmbyApiClient return None; } } + + private static List ProjectToModel( + IEnumerable embyChapters, + TimeSpan duration) + { + var models = embyChapters.Map(ProjectToModel).OrderBy(c => c.StartTime).ToList(); + + for (var index = 0; index < models.Count; index++) + { + MediaChapter model = models[index]; + model.ChapterId = index; + model.EndTime = index == models.Count - 1 ? duration : models[index + 1].StartTime; + } + + return models; + } + + private static MediaChapter ProjectToModel(EmbyChapterResponse chapterResponse) => + new() + { + Title = chapterResponse.Name, + StartTime = TimeSpan.FromTicks(chapterResponse.StartPositionTicks) + }; private MovieMetadata ProjectToMovieMetadata(EmbyLibraryItemResponse item) { @@ -644,10 +690,11 @@ public class EmbyApiClient : IEmbyApiClient } } + var duration = TimeSpan.FromTicks(item.RunTimeTicks); var version = new MediaVersion { Name = "Main", - Duration = TimeSpan.FromTicks(item.RunTimeTicks), + Duration = duration, DateAdded = item.DateCreated.UtcDateTime, MediaFiles = new List { @@ -656,7 +703,8 @@ public class EmbyApiClient : IEmbyApiClient Path = path } }, - Streams = new List() + Streams = new List(), + Chapters = ProjectToModel(Optional(item.Chapters).Flatten(), duration) }; EpisodeMetadata metadata = ProjectToEpisodeMetadata(item); @@ -750,4 +798,120 @@ public class EmbyApiClient : IEmbyApiClient return result; } + + private Option ProjectToMediaVersion(EmbyPlaybackInfoResponse response) + { + if (response.MediaSources is null || response.MediaSources.Count == 0) + { + _logger.LogWarning("Received empty playback info from Jellyfin"); + return None; + } + + EmbyMediaSourceResponse mediaSource = response.MediaSources.Head(); + IList streams = mediaSource.MediaStreams; + Option maybeVideoStream = + streams.Find(s => s.Type == EmbyMediaStreamType.Video); + return maybeVideoStream.Map( + videoStream => + { + int width = videoStream.Width ?? 1; + int height = videoStream.Height ?? 1; + + var isAnamorphic = false; + if (videoStream.IsAnamorphic.HasValue) + { + isAnamorphic = videoStream.IsAnamorphic.Value; + } + else if (!string.IsNullOrWhiteSpace(videoStream.AspectRatio) && videoStream.AspectRatio.Contains(":")) + { + // if width/height != aspect ratio, is anamorphic + double resolutionRatio = width / (double)height; + + string[] split = videoStream.AspectRatio.Split(":"); + var num = double.Parse(split[0]); + var den = double.Parse(split[1]); + double aspectRatio = num / den; + + isAnamorphic = Math.Abs(resolutionRatio - aspectRatio) > 0.01d; + } + + var version = new MediaVersion + { + Duration = TimeSpan.FromTicks(mediaSource.RunTimeTicks), + SampleAspectRatio = isAnamorphic ? "0:0" : "1:1", + DisplayAspectRatio = string.IsNullOrWhiteSpace(videoStream.AspectRatio) + ? string.Empty + : videoStream.AspectRatio, + VideoScanKind = videoStream.IsInterlaced switch + { + true => VideoScanKind.Interlaced, + false => VideoScanKind.Progressive + }, + Streams = new List(), + Width = videoStream.Width ?? 1, + Height = videoStream.Height ?? 1, + RFrameRate = videoStream.RealFrameRate.HasValue + ? videoStream.RealFrameRate.Value.ToString("0.00###", CultureInfo.InvariantCulture) + : string.Empty, + Chapters = new List() + }; + + version.Streams.Add( + new MediaStream + { + MediaVersionId = version.Id, + MediaStreamKind = MediaStreamKind.Video, + Index = videoStream.Index, + Codec = videoStream.Codec, + Profile = (videoStream.Profile ?? string.Empty).ToLowerInvariant(), + Default = videoStream.IsDefault, + Language = videoStream.Language, + Forced = videoStream.IsForced, + PixelFormat = videoStream.PixelFormat, + ColorRange = (videoStream.ColorRange ?? string.Empty).ToLowerInvariant(), + ColorSpace = (videoStream.ColorSpace ?? string.Empty).ToLowerInvariant(), + ColorTransfer = (videoStream.ColorTransfer ?? string.Empty).ToLowerInvariant(), + ColorPrimaries = (videoStream.ColorPrimaries ?? string.Empty).ToLowerInvariant() + }); + + foreach (EmbyMediaStreamResponse audioStream in streams.Filter( + s => s.Type == EmbyMediaStreamType.Audio)) + { + var stream = new MediaStream + { + MediaVersionId = version.Id, + MediaStreamKind = MediaStreamKind.Audio, + Index = audioStream.Index, + Codec = audioStream.Codec, + Profile = (audioStream.Profile ?? string.Empty).ToLowerInvariant(), + Channels = audioStream.Channels ?? 2, + Default = audioStream.IsDefault, + Forced = audioStream.IsForced, + Language = audioStream.Language, + Title = audioStream.DisplayTitle ?? string.Empty + }; + + version.Streams.Add(stream); + } + + foreach (EmbyMediaStreamResponse subtitleStream in streams.Filter( + s => s.Type == EmbyMediaStreamType.Subtitle)) + { + var stream = new MediaStream + { + MediaVersionId = version.Id, + MediaStreamKind = MediaStreamKind.Subtitle, + Index = subtitleStream.Index, + Codec = subtitleStream.Codec, + Default = subtitleStream.IsDefault, + Forced = subtitleStream.IsForced, + Language = subtitleStream.Language + }; + + version.Streams.Add(stream); + } + + return version; + }); + } } diff --git a/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs b/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs index 1d1f4613..9e37ad90 100644 --- a/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs +++ b/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs @@ -40,7 +40,7 @@ public interface IEmbyApi string parentId, [Query] string fields = - "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating,ProviderIds", + "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating,ProviderIds,Chapters", [Query] string includeItemTypes = "Movie", [Query] @@ -89,7 +89,7 @@ public interface IEmbyApi string seasonId, [Query] string fields = - "Path,Genres,Tags,DateCreated,Etag,Overview,ProductionYear,PremiereDate,MediaSources,LocationType,ProviderIds,People", + "Path,Genres,Tags,DateCreated,Etag,Overview,ProductionYear,PremiereDate,MediaSources,LocationType,ProviderIds,People,Chapters", [Query] int startIndex = 0, [Query] @@ -130,4 +130,11 @@ public interface IEmbyApi int startIndex = 0, [Query] int limit = 0); + + + [Get("/Items/{itemId}/PlaybackInfo")] + public Task GetPlaybackInfo( + [Header("X-Emby-Token")] + string apiKey, + string itemId); } diff --git a/ErsatzTV.Infrastructure/Emby/Models/EmbyChapterResponse.cs b/ErsatzTV.Infrastructure/Emby/Models/EmbyChapterResponse.cs new file mode 100644 index 00000000..143709a2 --- /dev/null +++ b/ErsatzTV.Infrastructure/Emby/Models/EmbyChapterResponse.cs @@ -0,0 +1,7 @@ +namespace ErsatzTV.Infrastructure.Emby.Models; + +public class EmbyChapterResponse +{ + public long StartPositionTicks { get; set; } + public string Name { get; set; } +} diff --git a/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs b/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs index 8ddf2169..84f72b08 100644 --- a/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs +++ b/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs @@ -14,7 +14,6 @@ public class EmbyLibraryItemResponse public int ProductionYear { get; set; } public EmbyProviderIdsResponse ProviderIds { get; set; } public string PremiereDate { get; set; } - public List MediaStreams { get; set; } public List MediaSources { get; set; } public string LocationType { get; set; } public string Overview { get; set; } @@ -25,4 +24,5 @@ public class EmbyLibraryItemResponse public List BackdropImageTags { get; set; } public int? IndexNumber { get; set; } public string Type { get; set; } + public IList Chapters { get; set; } } diff --git a/ErsatzTV.Infrastructure/Emby/Models/EmbyMediaSourceResponse.cs b/ErsatzTV.Infrastructure/Emby/Models/EmbyMediaSourceResponse.cs index 587615bb..e2f53848 100644 --- a/ErsatzTV.Infrastructure/Emby/Models/EmbyMediaSourceResponse.cs +++ b/ErsatzTV.Infrastructure/Emby/Models/EmbyMediaSourceResponse.cs @@ -4,4 +4,6 @@ public class EmbyMediaSourceResponse { public string Id { get; set; } public string Protocol { get; set; } + public long RunTimeTicks { get; set; } + public IList MediaStreams { get; set; } } diff --git a/ErsatzTV.Infrastructure/Emby/Models/EmbyMediaStreamResponse.cs b/ErsatzTV.Infrastructure/Emby/Models/EmbyMediaStreamResponse.cs index c35f7e7b..478834cf 100644 --- a/ErsatzTV.Infrastructure/Emby/Models/EmbyMediaStreamResponse.cs +++ b/ErsatzTV.Infrastructure/Emby/Models/EmbyMediaStreamResponse.cs @@ -14,4 +14,12 @@ public class EmbyMediaStreamResponse public string Profile { get; set; } public string AspectRatio { get; set; } public int? Channels { get; set; } + public bool? IsAnamorphic { get; set; } + public string DisplayTitle { get; set; } + public string PixelFormat { get; set; } + public string ColorRange { get; set; } + public string ColorSpace { get; set; } + public string ColorTransfer { get; set; } + public string ColorPrimaries { get; set; } + public double? RealFrameRate { get; set; } } diff --git a/ErsatzTV.Infrastructure/Emby/Models/EmbyPlaybackInfoResponse.cs b/ErsatzTV.Infrastructure/Emby/Models/EmbyPlaybackInfoResponse.cs new file mode 100644 index 00000000..ccbd7b31 --- /dev/null +++ b/ErsatzTV.Infrastructure/Emby/Models/EmbyPlaybackInfoResponse.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.Infrastructure.Emby.Models; + +public class EmbyPlaybackInfoResponse +{ + public IList MediaSources { get; set; } +} diff --git a/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs index 3781a852..6f1ea47c 100644 --- a/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs @@ -20,6 +20,7 @@ public class EmbyMovieLibraryScanner : private readonly IEmbyMovieRepository _embyMovieRepository; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IEmbyPathReplacementService _pathReplacementService; + private readonly ILogger _logger; public EmbyMovieLibraryScanner( IEmbyApiClient embyApiClient, @@ -44,7 +45,10 @@ public class EmbyMovieLibraryScanner : _mediaSourceRepository = mediaSourceRepository; _embyMovieRepository = embyMovieRepository; _pathReplacementService = pathReplacementService; + _logger = logger; } + + protected override bool ServerSupportsRemoteStreaming => true; public async Task> ScanLibrary( string address, @@ -110,6 +114,35 @@ public class EmbyMovieLibraryScanner : EmbyLibrary library, MediaItemScanResult result, EmbyMovie incoming) => Task.FromResult(Option>.None); + + protected override async Task> GetMediaServerStatistics( + EmbyConnectionParameters connectionParameters, + EmbyLibrary library, + MediaItemScanResult result, + EmbyMovie incoming) + { + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Emby Statistics", result.LocalPath); + + Either maybeVersion = + await _embyApiClient.GetPlaybackInfo( + connectionParameters.Address, + connectionParameters.ApiKey, + library, + incoming.ItemId); + + foreach (BaseError error in maybeVersion.LeftToSeq()) + { + _logger.LogWarning("Failed to get movie statistics from Emby: {Error}", error.ToString()); + } + + // chapters are pulled with metadata, not with statistics, but we need to save them here + foreach (MediaVersion version in maybeVersion.RightToSeq()) + { + version.Chapters = result.Item.GetHeadVersion().Chapters; + } + + return maybeVersion.ToOption(); + } protected override Task>> UpdateMetadata( MediaItemScanResult result, diff --git a/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs index eeb4f62f..218604d5 100644 --- a/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs @@ -19,6 +19,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< private readonly IEmbyApiClient _embyApiClient; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IEmbyPathReplacementService _pathReplacementService; + private readonly ILogger _logger; private readonly IEmbyTelevisionRepository _televisionRepository; public EmbyTelevisionLibraryScanner( @@ -44,8 +45,11 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< _mediaSourceRepository = mediaSourceRepository; _televisionRepository = televisionRepository; _pathReplacementService = pathReplacementService; + _logger = logger; } + protected override bool ServerSupportsRemoteStreaming => true; + public async Task> ScanLibrary( string address, string apiKey, @@ -170,6 +174,37 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< EmbyLibrary library, MediaItemScanResult result, EmbyEpisode incoming) => Task.FromResult(Option>.None); + + protected override async Task> GetMediaServerStatistics( + EmbyConnectionParameters connectionParameters, + EmbyLibrary library, + MediaItemScanResult result, + EmbyEpisode incoming) + { + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Emby Statistics", result.LocalPath); + + Either maybeVersion = + await _embyApiClient.GetPlaybackInfo( + connectionParameters.Address, + connectionParameters.ApiKey, + library, + incoming.ItemId); + + foreach (BaseError error in maybeVersion.LeftToSeq()) + { + _logger.LogWarning("Failed to get episode statistics from Emby: {Error}", error.ToString()); + } + + // chapters are pulled with metadata, not with statistics, but we need to save them here + foreach (MediaVersion version in maybeVersion.RightToSeq()) + { + version.Chapters = result.Item.GetHeadVersion().Chapters; + } + + return maybeVersion.ToOption(); + } + + protected override Task>> UpdateMetadata( MediaItemScanResult result, diff --git a/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs index 1e33d43a..b08efbc2 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs @@ -449,32 +449,32 @@ public abstract class MediaServerMovieLibraryScanner refreshResult = - await _localStatisticsProvider.RefreshStatistics( - ffmpegPath, - ffprobePath, - existing, - result.LocalPath); - - foreach (BaseError error in refreshResult.LeftToSeq()) - { - _logger.LogWarning( - "Unable to refresh {Attribute} for media item {Path}. Error: {Error}", - "Statistics", - result.LocalPath, - error.Value); - } - - foreach (bool _ in refreshResult.RightToSeq()) - { - result.IsUpdated = true; - } - } - else - { + // if (maybeMediaVersion.IsNone && _localFileSystem.FileExists(result.LocalPath)) + // { + // _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", result.LocalPath); + // Either refreshResult = + // await _localStatisticsProvider.RefreshStatistics( + // ffmpegPath, + // ffprobePath, + // existing, + // result.LocalPath); + // + // foreach (BaseError error in refreshResult.LeftToSeq()) + // { + // _logger.LogWarning( + // "Unable to refresh {Attribute} for media item {Path}. Error: {Error}", + // "Statistics", + // result.LocalPath, + // error.Value); + // } + // + // foreach (bool _ in refreshResult.RightToSeq()) + // { + // result.IsUpdated = true; + // } + // } + // else + // { if (maybeMediaVersion.IsNone) { maybeMediaVersion = await GetMediaServerStatistics( @@ -491,7 +491,7 @@ public abstract class MediaServerMovieLibraryScanner refreshResult = - await _localStatisticsProvider.RefreshStatistics( - ffmpegPath, - ffprobePath, - existing, - result.LocalPath); - - foreach (BaseError error in refreshResult.LeftToSeq()) - { - _logger.LogWarning( - "Unable to refresh {Attribute} for media item {Path}. Error: {Error}", - "Statistics", - result.LocalPath, - error.Value); - } - - foreach (bool _ in refreshResult.RightToSeq()) - { - result.IsUpdated = true; - } - } - else - { + // if (maybeMediaVersion.IsNone && _localFileSystem.FileExists(result.LocalPath)) + // { + // _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", result.LocalPath); + // Either refreshResult = + // await _localStatisticsProvider.RefreshStatistics( + // ffmpegPath, + // ffprobePath, + // existing, + // result.LocalPath); + // + // foreach (BaseError error in refreshResult.LeftToSeq()) + // { + // _logger.LogWarning( + // "Unable to refresh {Attribute} for media item {Path}. Error: {Error}", + // "Statistics", + // result.LocalPath, + // error.Value); + // } + // + // foreach (bool _ in refreshResult.RightToSeq()) + // { + // result.IsUpdated = true; + // } + // } + // else + // { if (maybeMediaVersion.IsNone) { maybeMediaVersion = await GetMediaServerStatistics( @@ -825,7 +825,7 @@ public abstract class MediaServerTelevisionLibraryScanner GetEmbyMedia(string path, CancellationToken cancellationToken) + { + Either connectionParameters = + await _mediator.Send(new GetEmbyConnectionParameters(), cancellationToken); + + return connectionParameters.Match( + Left: _ => new NotFoundResult(), + Right: r => + { + Url fullPath = Flurl.Url.Parse(r.Address) + .AppendPathSegment("Videos") + .AppendPathSegment(path) + .AppendPathSegment("stream") + .SetQueryParam("static", "true") + .SetQueryParam("X-Emby-Token", r.ApiKey); + + return new RedirectResult(fullPath.ToString()); + }); + } }