From 73c6758537ba656a41a52547d9b40f90d0cae9eb Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Thu, 20 Apr 2023 10:45:32 -0500 Subject: [PATCH] add show media info button to movie and episode detail pages (#1253) --- CHANGELOG.md | 1 + .../MediaItems/MediaItemInfo.cs | 19 ++++ .../MediaItems/MediaItemInfoStream.cs | 23 +++++ .../MediaItems/Queries/GetMediaItemInfo.cs | 5 + .../Queries/GetMediaItemInfoHandler.cs | 97 +++++++++++++++++++ ErsatzTV/Pages/Movie.razor | 32 +++++- ErsatzTV/Pages/TelevisionEpisodeList.razor | 32 +++++- ErsatzTV/Shared/MediaItemInfoDialog.razor | 62 ++++++++++++ 8 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 ErsatzTV.Application/MediaItems/MediaItemInfo.cs create mode 100644 ErsatzTV.Application/MediaItems/MediaItemInfoStream.cs create mode 100644 ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfo.cs create mode 100644 ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs create mode 100644 ErsatzTV/Shared/MediaItemInfoDialog.razor diff --git a/CHANGELOG.md b/CHANGELOG.md index 16ed4c581..fb7206303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add `Season, Episode` playback order - This is currently *only* available when a show is added directly to a schedule - This will ignore release date and sort exclusively by season number and then by episode number +- Add `Show Media Info` button to movie and episode detail pages for troubleshooting ### Fixed - Limit `HLS Direct` streams to realtime speed diff --git a/ErsatzTV.Application/MediaItems/MediaItemInfo.cs b/ErsatzTV.Application/MediaItems/MediaItemInfo.cs new file mode 100644 index 000000000..90fb1e982 --- /dev/null +++ b/ErsatzTV.Application/MediaItems/MediaItemInfo.cs @@ -0,0 +1,19 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Application.MediaItems; + +public record MediaItemInfo( + int Id, + string Kind, + string LibraryKind, + string ServerName, + string LibraryName, + MediaItemState State, + TimeSpan Duration, + string SampleAspectRatio, + string DisplayAspectRatio, + string RFrameRate, + VideoScanKind VideoScanKind, + int Width, + int Height, + List Streams); diff --git a/ErsatzTV.Application/MediaItems/MediaItemInfoStream.cs b/ErsatzTV.Application/MediaItems/MediaItemInfoStream.cs new file mode 100644 index 000000000..819700dbd --- /dev/null +++ b/ErsatzTV.Application/MediaItems/MediaItemInfoStream.cs @@ -0,0 +1,23 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Application.MediaItems; + +public record MediaItemInfoStream( + int Index, + MediaStreamKind Kind, + string Title, + string Codec, + string Profile, + string Language, + int? Channels, + bool Default, + bool Forced, + bool AttachedPic, + string PixelFormat, + string ColorRange, + string ColorSpace, + string ColorTransfer, + string ColorPrimaries, + int BitsPerRawSample, + string FileName, + string MimeType); diff --git a/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfo.cs b/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfo.cs new file mode 100644 index 000000000..e7ba759f7 --- /dev/null +++ b/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfo.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.MediaItems; + +public record GetMediaItemInfo(int Id) : IRequest>; diff --git a/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs b/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs new file mode 100644 index 000000000..98b95089e --- /dev/null +++ b/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs @@ -0,0 +1,97 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.MediaItems; + +public class GetMediaItemInfoHandler : IRequestHandler> +{ + private readonly IDbContextFactory _dbContextFactory; + + public GetMediaItemInfoHandler(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public async Task> Handle( + GetMediaItemInfo request, + CancellationToken cancellationToken) + { + try + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + Option mediaItem = await dbContext.MediaItems + .AsNoTracking() + .Include(i => i.LibraryPath) + .ThenInclude(lp => lp.Library) + .ThenInclude(l => l.MediaSource) + // TODO: support all media types here + .Include(i => (i as Movie).MediaVersions) + .ThenInclude(mv => mv.Streams) + .Include(i => (i as Episode).MediaVersions) + .ThenInclude(mv => mv.Streams) + .SelectOneAsync(i => i.Id, i => i.Id == request.Id) + .MapT(Project); + + return mediaItem.ToEither(BaseError.New("Unable to locate media item")); + } + catch (Exception ex) + { + return BaseError.New(ex.ToString()); + } + } + + private static MediaItemInfo Project(MediaItem mediaItem) + { + MediaVersion version = mediaItem.GetHeadVersion(); + + string serverName = mediaItem.LibraryPath.Library.MediaSource switch + { + PlexMediaSource plexMediaSource => plexMediaSource.ServerName, + EmbyMediaSource embyMediaSource => embyMediaSource.ServerName, + JellyfinMediaSource jellyfinMediaSource => jellyfinMediaSource.ServerName, + _ => null + }; + + return new MediaItemInfo( + mediaItem.Id, + mediaItem.GetType().Name, + mediaItem.LibraryPath.Library.GetType().Name, + serverName, + mediaItem.LibraryPath.Library.Name, + mediaItem.State, + version.Duration, + version.SampleAspectRatio, + version.DisplayAspectRatio, + version.RFrameRate, + version.VideoScanKind, + version.Width, + version.Height, + version.Streams.OrderBy(s => s.Index).Map(Project).ToList()); + } + + private static MediaItemInfoStream Project(MediaStream mediaStream) => + new( + mediaStream.Index, + mediaStream.MediaStreamKind, + mediaStream.Title, + mediaStream.Codec, + mediaStream.Profile, + mediaStream.Language, + mediaStream.Channels > 0 ? mediaStream.Channels : null, + mediaStream.Default, + mediaStream.Forced, + mediaStream.AttachedPic, + mediaStream.PixelFormat, + mediaStream.ColorRange, + mediaStream.ColorSpace, + mediaStream.ColorTransfer, + mediaStream.ColorPrimaries, + mediaStream.BitsPerRawSample, + mediaStream.FileName, + mediaStream.MimeType); +} diff --git a/ErsatzTV/Pages/Movie.razor b/ErsatzTV/Pages/Movie.razor index fded461ca..057d00cae 100644 --- a/ErsatzTV/Pages/Movie.razor +++ b/ErsatzTV/Pages/Movie.razor @@ -4,10 +4,13 @@ @using System.Globalization @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaItems @implements IDisposable @inject IMediator _mediator @inject IDialogService _dialog @inject NavigationManager _navigationManager +@inject ILogger _logger +@inject ISnackbar _snackbar
@@ -66,13 +69,22 @@ }
- Add To Collection
+
+ + Show Media Info + +
@if (_movie.MediaItemState == MediaItemState.FileNotFound) @@ -253,4 +265,22 @@ } } + private async Task ShowInfo() + { + Either maybeInfo = await _mediator.Send(new GetMediaItemInfo(MovieId)); + + foreach (BaseError error in maybeInfo.LeftToSeq()) + { + _snackbar.Add("Unexpected error loading media info"); + _logger.LogError("Unexpected error loading media info: {Error}", error.Value); + } + + foreach (MediaItemInfo info in maybeInfo.RightToSeq()) + { + var parameters = new DialogParameters { { "MediaItemInfo", info } }; + var options = new DialogOptions { CloseButton = true, CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; + IDialogReference dialog = await _dialog.ShowAsync(_movie.Title, parameters, options); + DialogResult _ = await dialog.Result; + } + } } \ No newline at end of file diff --git a/ErsatzTV/Pages/TelevisionEpisodeList.razor b/ErsatzTV/Pages/TelevisionEpisodeList.razor index c41b8d492..4f0861915 100644 --- a/ErsatzTV/Pages/TelevisionEpisodeList.razor +++ b/ErsatzTV/Pages/TelevisionEpisodeList.razor @@ -3,6 +3,7 @@ @using ErsatzTV.Application.Television @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaItems @using ErsatzTV.Application.ProgramSchedules @implements IDisposable @inject IMediator _mediator @@ -10,7 +11,6 @@ @inject ISnackbar _snackbar @inject IDialogService _dialog @inject NavigationManager _navigationManager -@inject ChannelWriter _channel @inject IJSRuntime _jsRuntime @@ -109,6 +109,14 @@ Add To Collection +
+ + Show Media Info + +
@@ -258,5 +266,27 @@ Right: _ => _snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success)); } } + + private async Task ShowInfo(TelevisionEpisodeCardViewModel episode) + { + Either maybeInfo = await _mediator.Send(new GetMediaItemInfo(episode.EpisodeId)); + + foreach (BaseError error in maybeInfo.LeftToSeq()) + { + _snackbar.Add("Unexpected error loading media info"); + _logger.LogError("Unexpected error loading media info: {Error}", error.Value); + } + + foreach (MediaItemInfo info in maybeInfo.RightToSeq()) + { + var parameters = new DialogParameters { { "MediaItemInfo", info } }; + var options = new DialogOptions { CloseButton = true, CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; + IDialogReference dialog = await _dialog.ShowAsync( + $"{episode.ShowTitle} - s{episode.Season:00}e{episode.Episode:00} - {episode.Title}", + parameters, + options); + DialogResult _ = await dialog.Result; + } + } } \ No newline at end of file diff --git a/ErsatzTV/Shared/MediaItemInfoDialog.razor b/ErsatzTV/Shared/MediaItemInfoDialog.razor new file mode 100644 index 000000000..8725247ce --- /dev/null +++ b/ErsatzTV/Shared/MediaItemInfoDialog.razor @@ -0,0 +1,62 @@ +@using ErsatzTV.Application.MediaItems +@using System.Text.Json +@using System.Text.Json.Serialization +@inject IJSRuntime JsRuntime + +
+ + +
+
+                    @_info
+                
+
+
+ + + Copy + + Close + +
+
+ +@code { + + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } + + [Parameter] + public MediaItemInfo MediaItemInfo { get; set; } + + private string _info; + private ElementReference _infoView; + + protected override Task OnParametersSetAsync() + { + try + { + _info = JsonSerializer.Serialize( + MediaItemInfo, + new JsonSerializerOptions + { + Converters = { new JsonStringEnumConverter() }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true + }); + } + catch (Exception ex) + { + _info = ex.ToString(); + } + + return Task.CompletedTask; + } + + private async Task CopyToClipboard(ElementReference view) + { + await JsRuntime.InvokeVoidAsync("clipboardCopy.copyText", view); + } + + private void Close() => MudDialog.Close(DialogResult.Ok(true)); +} \ No newline at end of file