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
|
- Add `Season, Episode` playback order
|
||||||
- This is currently *only* available when a show is added directly to a schedule
|
- 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
|
- 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
|
### Fixed
|
||||||
- Limit `HLS Direct` streams to realtime speed
|
- 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 System.Globalization
|
||||||
@using ErsatzTV.Application.MediaCards
|
@using ErsatzTV.Application.MediaCards
|
||||||
@using ErsatzTV.Application.MediaCollections
|
@using ErsatzTV.Application.MediaCollections
|
||||||
|
@using ErsatzTV.Application.MediaItems
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@inject IMediator _mediator
|
@inject IMediator _mediator
|
||||||
@inject IDialogService _dialog
|
@inject IDialogService _dialog
|
||||||
@inject NavigationManager _navigationManager
|
@inject NavigationManager _navigationManager
|
||||||
|
@inject ILogger<Movie> _logger
|
||||||
|
@inject ISnackbar _snackbar
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
|
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
|
||||||
<div class="fanart-tint"></div>
|
<div class="fanart-tint"></div>
|
||||||
@@ -66,13 +69,22 @@
|
|||||||
</MudCard>
|
</MudCard>
|
||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
<MudButton Variant="Variant.Filled"
|
<MudButton Class="mb-6"
|
||||||
|
Variant="Variant.Filled"
|
||||||
Color="Color.Primary"
|
Color="Color.Primary"
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
StartIcon="@Icons.Material.Filled.Add"
|
||||||
OnClick="@AddToCollection">
|
OnClick="@AddToCollection">
|
||||||
Add To Collection
|
Add To Collection
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Secondary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Info"
|
||||||
|
OnClick="@ShowInfo">
|
||||||
|
Show Media Info
|
||||||
|
</MudButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (_movie.MediaItemState == MediaItemState.FileNotFound)
|
@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.Television
|
||||||
@using ErsatzTV.Application.MediaCards
|
@using ErsatzTV.Application.MediaCards
|
||||||
@using ErsatzTV.Application.MediaCollections
|
@using ErsatzTV.Application.MediaCollections
|
||||||
|
@using ErsatzTV.Application.MediaItems
|
||||||
@using ErsatzTV.Application.ProgramSchedules
|
@using ErsatzTV.Application.ProgramSchedules
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@inject IMediator _mediator
|
@inject IMediator _mediator
|
||||||
@@ -10,7 +11,6 @@
|
|||||||
@inject ISnackbar _snackbar
|
@inject ISnackbar _snackbar
|
||||||
@inject IDialogService _dialog
|
@inject IDialogService _dialog
|
||||||
@inject NavigationManager _navigationManager
|
@inject NavigationManager _navigationManager
|
||||||
@inject ChannelWriter<IBackgroundServiceRequest> _channel
|
|
||||||
@inject IJSRuntime _jsRuntime
|
@inject IJSRuntime _jsRuntime
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
|
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
|
||||||
@@ -109,6 +109,14 @@
|
|||||||
Add To Collection
|
Add To Collection
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,5 +266,27 @@
|
|||||||
Right: _ => _snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success));
|
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