add show media info button to movie and episode detail pages (#1253)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<MediaItemInfoStream> Streams);
|
||||
@@ -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);
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems;
|
||||
|
||||
public record GetMediaItemInfo(int Id) : IRequest<Either<BaseError, MediaItemInfo>>;
|
||||
@@ -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<GetMediaItemInfo, Either<BaseError, MediaItemInfo>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetMediaItemInfoHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, MediaItemInfo>> Handle(
|
||||
GetMediaItemInfo request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Option<MediaItemInfo> 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);
|
||||
}
|
||||
@@ -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<Movie> _logger
|
||||
@inject ISnackbar _snackbar
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
|
||||
<div class="fanart-tint"></div>
|
||||
@@ -66,13 +69,22 @@
|
||||
</MudCard>
|
||||
}
|
||||
<div>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
<MudButton Class="mb-6"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@AddToCollection">
|
||||
Add To Collection
|
||||
</MudButton>
|
||||
</div>
|
||||
<div>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.Info"
|
||||
OnClick="@ShowInfo">
|
||||
Show Media Info
|
||||
</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (_movie.MediaItemState == MediaItemState.FileNotFound)
|
||||
@@ -253,4 +265,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowInfo()
|
||||
{
|
||||
Either<BaseError, MediaItemInfo> 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<MediaItemInfoDialog>(_movie.Title, parameters, options);
|
||||
DialogResult _ = await dialog.Result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IBackgroundServiceRequest> _channel
|
||||
@inject IJSRuntime _jsRuntime
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
|
||||
@@ -109,6 +109,14 @@
|
||||
Add To Collection
|
||||
</MudButton>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.Info"
|
||||
OnClick="@(_ => ShowInfo(episode))">
|
||||
Show Media Info
|
||||
</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
</MudCardContent>
|
||||
</div>
|
||||
@@ -258,5 +266,27 @@
|
||||
Right: _ => _snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowInfo(TelevisionEpisodeCardViewModel episode)
|
||||
{
|
||||
Either<BaseError, MediaItemInfo> 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<MediaItemInfoDialog>(
|
||||
$"{episode.ShowTitle} - s{episode.Season:00}e{episode.Episode:00} - {episode.Title}",
|
||||
parameters,
|
||||
options);
|
||||
DialogResult _ = await dialog.Result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
@using ErsatzTV.Application.MediaItems
|
||||
@using System.Text.Json
|
||||
@using System.Text.Json.Serialization
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<div>
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<div class="overflow-y-scroll" style="max-height: 500px">
|
||||
<pre>
|
||||
<code @ref="_infoView">@_info</code>
|
||||
</pre>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="(() => CopyToClipboard(_infoView))">
|
||||
Copy
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Primary" OnClick="Close">Close</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</div>
|
||||
|
||||
@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));
|
||||
}
|
||||
Reference in New Issue
Block a user