Browse Source

direct stream content from emby as needed (#1168)

pull/1171/head
Jason Dove 2 years ago committed by GitHub
parent
commit
7e3436e68f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs
  3. 27
      ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs
  4. 15
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  5. 8
      ErsatzTV.Core/Emby/EmbyMediaStreamType.cs
  6. 2
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  7. 6
      ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs
  8. 3
      ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs
  9. 3
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  10. 1
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  11. 174
      ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs
  12. 11
      ErsatzTV.Infrastructure/Emby/IEmbyApi.cs
  13. 7
      ErsatzTV.Infrastructure/Emby/Models/EmbyChapterResponse.cs
  14. 2
      ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs
  15. 2
      ErsatzTV.Infrastructure/Emby/Models/EmbyMediaSourceResponse.cs
  16. 8
      ErsatzTV.Infrastructure/Emby/Models/EmbyMediaStreamResponse.cs
  17. 6
      ErsatzTV.Infrastructure/Emby/Models/EmbyPlaybackInfoResponse.cs
  18. 33
      ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs
  19. 35
      ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs
  20. 54
      ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs
  21. 54
      ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  22. 22
      ErsatzTV/Controllers/InternalController.cs

8
CHANGELOG.md

@ -8,14 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

2
ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Emby;
public record EmbyConnectionParametersViewModel(string Address);
public record EmbyConnectionParametersViewModel(string Address, string ApiKey);

27
ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs

@ -1,5 +1,7 @@ @@ -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<GetEmbyConnect @@ -9,14 +11,17 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
Either<BaseError, EmbyConnectionParametersViewModel>>
{
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<Either<BaseError, EmbyConnectionParametersViewModel>> Handle(
@ -30,7 +35,7 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect @@ -30,7 +35,7 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
Either<BaseError, EmbyConnectionParametersViewModel> maybeParameters =
await Validate()
.MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address))
.MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address, cp.ApiKey))
.Map(v => v.ToEither<EmbyConnectionParametersViewModel>());
return maybeParameters.Match(
@ -44,7 +49,8 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect @@ -44,7 +49,8 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
private Task<Validation<BaseError, ConnectionParameters>> Validate() =>
EmbyMediaSourceMustExist()
.BindT(MediaSourceMustHaveActiveConnection);
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
@ -59,8 +65,21 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect @@ -59,8 +65,21 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> 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<BaseError>("Emby media source requires an api key");
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection);
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
}

15
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -496,6 +496,21 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -496,6 +496,21 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
playoutItem,
$"http://localhost:{Settings.ListenPort}/media/jellyfin/{itemId}");
}
// attempt to remotely stream emby
Option<string> 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);
}

8
ErsatzTV.Core/Emby/EmbyMediaStreamType.cs

@ -0,0 +1,8 @@ @@ -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";
}

2
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -270,7 +270,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -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<MediaStream>.None;
}

6
ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs

@ -34,4 +34,10 @@ public interface IEmbyApiClient @@ -34,4 +34,10 @@ public interface IEmbyApiClient
string apiKey,
string parentId,
string includeItemTypes);
Task<Either<BaseError, MediaVersion>> GetPlaybackInfo(
string address,
string apiKey,
EmbyLibrary library,
string itemId);
}

3
ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs

@ -126,6 +126,8 @@ public class EmbyMovieRepository : IEmbyMovieRepository @@ -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 @@ -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();

3
ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs

@ -135,6 +135,8 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -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 @@ -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();

1
ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs

@ -729,6 +729,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -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();

174
ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs

@ -1,4 +1,5 @@ @@ -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 @@ -191,6 +192,26 @@ public class EmbyApiClient : IEmbyApiClient
}
}
public async Task<Either<BaseError, MediaVersion>> GetPlaybackInfo(
string address,
string apiKey,
EmbyLibrary library,
string itemId)
{
try
{
IEmbyApi service = RestService.For<IEmbyApi>(address);
EmbyPlaybackInfoResponse playbackInfo = await service.GetPlaybackInfo(apiKey, itemId);
Option<MediaVersion> 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<TItem> GetPagedLibraryContents<TItem>(
string address,
string apiKey,
@ -328,10 +349,11 @@ public class EmbyApiClient : IEmbyApiClient @@ -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<MediaFile>
{
@ -340,7 +362,8 @@ public class EmbyApiClient : IEmbyApiClient @@ -340,7 +362,8 @@ public class EmbyApiClient : IEmbyApiClient
Path = path
}
},
Streams = new List<MediaStream>()
Streams = new List<MediaStream>(),
Chapters = ProjectToModel(Optional(item.Chapters).Flatten(), duration)
};
MovieMetadata metadata = ProjectToMovieMetadata(item);
@ -362,6 +385,29 @@ public class EmbyApiClient : IEmbyApiClient @@ -362,6 +385,29 @@ public class EmbyApiClient : IEmbyApiClient
return None;
}
}
private static List<MediaChapter> ProjectToModel(
IEnumerable<EmbyChapterResponse> 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 @@ -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<MediaFile>
{
@ -656,7 +703,8 @@ public class EmbyApiClient : IEmbyApiClient @@ -656,7 +703,8 @@ public class EmbyApiClient : IEmbyApiClient
Path = path
}
},
Streams = new List<MediaStream>()
Streams = new List<MediaStream>(),
Chapters = ProjectToModel(Optional(item.Chapters).Flatten(), duration)
};
EpisodeMetadata metadata = ProjectToEpisodeMetadata(item);
@ -750,4 +798,120 @@ public class EmbyApiClient : IEmbyApiClient @@ -750,4 +798,120 @@ public class EmbyApiClient : IEmbyApiClient
return result;
}
private Option<MediaVersion> 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<EmbyMediaStreamResponse> streams = mediaSource.MediaStreams;
Option<EmbyMediaStreamResponse> 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<MediaStream>(),
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<MediaChapter>()
};
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;
});
}
}

11
ErsatzTV.Infrastructure/Emby/IEmbyApi.cs

@ -40,7 +40,7 @@ public interface IEmbyApi @@ -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 @@ -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 @@ -130,4 +130,11 @@ public interface IEmbyApi
int startIndex = 0,
[Query]
int limit = 0);
[Get("/Items/{itemId}/PlaybackInfo")]
public Task<EmbyPlaybackInfoResponse> GetPlaybackInfo(
[Header("X-Emby-Token")]
string apiKey,
string itemId);
}

7
ErsatzTV.Infrastructure/Emby/Models/EmbyChapterResponse.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.Infrastructure.Emby.Models;
public class EmbyChapterResponse
{
public long StartPositionTicks { get; set; }
public string Name { get; set; }
}

2
ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs

@ -14,7 +14,6 @@ public class EmbyLibraryItemResponse @@ -14,7 +14,6 @@ public class EmbyLibraryItemResponse
public int ProductionYear { get; set; }
public EmbyProviderIdsResponse ProviderIds { get; set; }
public string PremiereDate { get; set; }
public List<EmbyMediaStreamResponse> MediaStreams { get; set; }
public List<EmbyMediaSourceResponse> MediaSources { get; set; }
public string LocationType { get; set; }
public string Overview { get; set; }
@ -25,4 +24,5 @@ public class EmbyLibraryItemResponse @@ -25,4 +24,5 @@ public class EmbyLibraryItemResponse
public List<string> BackdropImageTags { get; set; }
public int? IndexNumber { get; set; }
public string Type { get; set; }
public IList<EmbyChapterResponse> Chapters { get; set; }
}

2
ErsatzTV.Infrastructure/Emby/Models/EmbyMediaSourceResponse.cs

@ -4,4 +4,6 @@ public class EmbyMediaSourceResponse @@ -4,4 +4,6 @@ public class EmbyMediaSourceResponse
{
public string Id { get; set; }
public string Protocol { get; set; }
public long RunTimeTicks { get; set; }
public IList<EmbyMediaStreamResponse> MediaStreams { get; set; }
}

8
ErsatzTV.Infrastructure/Emby/Models/EmbyMediaStreamResponse.cs

@ -14,4 +14,12 @@ public class EmbyMediaStreamResponse @@ -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; }
}

6
ErsatzTV.Infrastructure/Emby/Models/EmbyPlaybackInfoResponse.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Infrastructure.Emby.Models;
public class EmbyPlaybackInfoResponse
{
public IList<EmbyMediaSourceResponse> MediaSources { get; set; }
}

33
ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs

@ -20,6 +20,7 @@ public class EmbyMovieLibraryScanner : @@ -20,6 +20,7 @@ public class EmbyMovieLibraryScanner :
private readonly IEmbyMovieRepository _embyMovieRepository;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IEmbyPathReplacementService _pathReplacementService;
private readonly ILogger<EmbyMovieLibraryScanner> _logger;
public EmbyMovieLibraryScanner(
IEmbyApiClient embyApiClient,
@ -44,7 +45,10 @@ public class EmbyMovieLibraryScanner : @@ -44,7 +45,10 @@ public class EmbyMovieLibraryScanner :
_mediaSourceRepository = mediaSourceRepository;
_embyMovieRepository = embyMovieRepository;
_pathReplacementService = pathReplacementService;
_logger = logger;
}
protected override bool ServerSupportsRemoteStreaming => true;
public async Task<Either<BaseError, Unit>> ScanLibrary(
string address,
@ -110,6 +114,35 @@ public class EmbyMovieLibraryScanner : @@ -110,6 +114,35 @@ public class EmbyMovieLibraryScanner :
EmbyLibrary library,
MediaItemScanResult<EmbyMovie> result,
EmbyMovie incoming) => Task.FromResult(Option<Tuple<MovieMetadata, MediaVersion>>.None);
protected override async Task<Option<MediaVersion>> GetMediaServerStatistics(
EmbyConnectionParameters connectionParameters,
EmbyLibrary library,
MediaItemScanResult<EmbyMovie> result,
EmbyMovie incoming)
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Emby Statistics", result.LocalPath);
Either<BaseError, MediaVersion> 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<Either<BaseError, MediaItemScanResult<EmbyMovie>>> UpdateMetadata(
MediaItemScanResult<EmbyMovie> result,

35
ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -19,6 +19,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< @@ -19,6 +19,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
private readonly IEmbyApiClient _embyApiClient;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IEmbyPathReplacementService _pathReplacementService;
private readonly ILogger<EmbyTelevisionLibraryScanner> _logger;
private readonly IEmbyTelevisionRepository _televisionRepository;
public EmbyTelevisionLibraryScanner(
@ -44,8 +45,11 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< @@ -44,8 +45,11 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
_mediaSourceRepository = mediaSourceRepository;
_televisionRepository = televisionRepository;
_pathReplacementService = pathReplacementService;
_logger = logger;
}
protected override bool ServerSupportsRemoteStreaming => true;
public async Task<Either<BaseError, Unit>> ScanLibrary(
string address,
string apiKey,
@ -170,6 +174,37 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< @@ -170,6 +174,37 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
EmbyLibrary library,
MediaItemScanResult<EmbyEpisode> result,
EmbyEpisode incoming) => Task.FromResult(Option<Tuple<EpisodeMetadata, MediaVersion>>.None);
protected override async Task<Option<MediaVersion>> GetMediaServerStatistics(
EmbyConnectionParameters connectionParameters,
EmbyLibrary library,
MediaItemScanResult<EmbyEpisode> result,
EmbyEpisode incoming)
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Emby Statistics", result.LocalPath);
Either<BaseError, MediaVersion> 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<Either<BaseError, MediaItemScanResult<EmbyShow>>> UpdateMetadata(
MediaItemScanResult<EmbyShow> result,

54
ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs

@ -449,32 +449,32 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -449,32 +449,32 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
if (deepScan || result.IsAdded || MediaServerEtag(existing) != MediaServerEtag(incoming) ||
existing.MediaVersions.Head().Streams.Count == 0)
{
if (maybeMediaVersion.IsNone && _localFileSystem.FileExists(result.LocalPath))
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", result.LocalPath);
Either<BaseError, bool> 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<BaseError, bool> 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<TConnectionParameters, TLib @@ -491,7 +491,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
result.IsUpdated = true;
}
}
}
// }
}
return result;

54
ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -783,32 +783,32 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -783,32 +783,32 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
if (deepScan || result.IsAdded || MediaServerEtag(existing) != MediaServerEtag(incoming) ||
existing.MediaVersions.Head().Streams.Count == 0)
{
if (maybeMediaVersion.IsNone && _localFileSystem.FileExists(result.LocalPath))
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", result.LocalPath);
Either<BaseError, bool> 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<BaseError, bool> 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<TConnectionParameters, @@ -825,7 +825,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
result.IsUpdated = true;
}
}
}
// }
}
return result;

22
ErsatzTV/Controllers/InternalController.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System.Diagnostics;
using CliWrap;
using ErsatzTV.Application.Emby;
using ErsatzTV.Application.Jellyfin;
using ErsatzTV.Application.Plex;
using ErsatzTV.Application.Streaming;
@ -121,4 +122,25 @@ public class InternalController : ControllerBase @@ -121,4 +122,25 @@ public class InternalController : ControllerBase
return new RedirectResult(fullPath.ToString());
});
}
[HttpGet("/media/emby/{*path}")]
public async Task<IActionResult> GetEmbyMedia(string path, CancellationToken cancellationToken)
{
Either<BaseError, EmbyConnectionParametersViewModel> connectionParameters =
await _mediator.Send(new GetEmbyConnectionParameters(), cancellationToken);
return connectionParameters.Match<IActionResult>(
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());
});
}
}

Loading…
Cancel
Save