* initial plumbing * scan for plex networks * save network contents to db as tags * eliminate network tag id churn * add network and show_network to search index * update last networks scan * show networks on tv show page * update changelog
294 lines
14 KiB
Plaintext
294 lines
14 KiB
Plaintext
@page "/media/tv/shows/{ShowId:int}"
|
|
@using System.Globalization
|
|
@using ErsatzTV.Application.MediaCards
|
|
@using ErsatzTV.Application.MediaCollections
|
|
@using ErsatzTV.Application.ProgramSchedules
|
|
@using ErsatzTV.Application.Television
|
|
@using ErsatzTV.Extensions
|
|
@implements IDisposable
|
|
@inject IMediator Mediator
|
|
@inject ILogger<TelevisionSeasonList> Logger
|
|
@inject ISnackbar Snackbar
|
|
@inject IDialogService Dialog
|
|
@inject NavigationManager NavigationManager
|
|
|
|
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
|
|
<div class="fanart-tint"></div>
|
|
@if (!string.IsNullOrWhiteSpace(_show?.FanArt))
|
|
{
|
|
if (_show.FanArt.StartsWith("http://") || _show.FanArt.StartsWith("https://"))
|
|
{
|
|
<img src="@_show.FanArt" alt="fan art"/>
|
|
}
|
|
else
|
|
{
|
|
<img src="@($"artwork/fanart/{_show.FanArt}")" alt="fan art"/>
|
|
}
|
|
}
|
|
</MudContainer>
|
|
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 200px">
|
|
<div style="display: flex; flex-direction: row;" class="mb-6">
|
|
@if (!string.IsNullOrWhiteSpace(_show?.Poster))
|
|
{
|
|
if (_show.Poster.StartsWith("http://") || _show.Poster.StartsWith("https://"))
|
|
{
|
|
<img class="mud-elevation-2 mr-6"
|
|
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
|
|
src="@_show.Poster" alt="movie poster"/>
|
|
}
|
|
else
|
|
{
|
|
<img class="mud-elevation-2 mr-6"
|
|
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
|
|
src="@($"artwork/posters/{_show.Poster}")" alt="movie poster"/>
|
|
}
|
|
}
|
|
<div style="display: flex; flex-direction: column; height: 100%">
|
|
<MudText Typo="Typo.h2" Class="media-item-title">@_show?.Title</MudText>
|
|
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_show?.Year</MudText>
|
|
@if (!string.IsNullOrWhiteSpace(_show?.Plot))
|
|
{
|
|
<MudCard Elevation="2" Class="mb-6">
|
|
<MudCardContent Class="mx-3 my-3" Style="height: 100%">
|
|
<MudText Style="flex-grow: 1">@_show.Plot</MudText>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
}
|
|
<div>
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Add"
|
|
OnClick="@AddToCollection">
|
|
Add To Collection
|
|
</MudButton>
|
|
<MudButton Class="ml-3"
|
|
Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.PlaylistAdd"
|
|
OnClick="@AddToPlaylist">
|
|
Add To Playlist
|
|
</MudButton>
|
|
<MudButton Class="ml-3"
|
|
Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Schedule"
|
|
OnClick="@AddToSchedule">
|
|
Add To Schedule
|
|
</MudButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<MudCard Class="mb-6">
|
|
<MudCardContent>
|
|
@if (_sortedContentRatings.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Content Ratings: </MudText>
|
|
<MudLink Href="@(@$"content_rating:""{_sortedContentRatings.Head()}""".GetRelativeSearchQuery())">@_sortedContentRatings.Head()</MudLink>
|
|
@foreach (string contentRating in _sortedContentRatings.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"content_rating:""{contentRating}""".GetRelativeSearchQuery())">@contentRating</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedLanguages.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Languages: </MudText>
|
|
<MudLink Href="@(@$"language:""{_sortedLanguages.Head().EnglishName.ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedLanguages.Head().EnglishName</MudLink>
|
|
@foreach (CultureInfo language in _sortedLanguages.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"language:""{language.EnglishName.ToLowerInvariant()}""".GetRelativeSearchQuery())">@language.EnglishName</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedStudios.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Studios: </MudText>
|
|
<MudLink Href="@(@$"studio:""{_sortedStudios.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedStudios.Head()</MudLink>
|
|
@foreach (string studio in _sortedStudios.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"studio:""{studio.ToLowerInvariant()}""".GetRelativeSearchQuery())">@studio</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedNetworks.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Networks: </MudText>
|
|
<MudLink Href="@(@$"network:""{_sortedNetworks.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedNetworks.Head()</MudLink>
|
|
@foreach (string network in _sortedNetworks.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"network:""{network.ToLowerInvariant()}""".GetRelativeSearchQuery())">@network</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedGenres.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Genres: </MudText>
|
|
<MudLink Href="@(@$"genre:""{_sortedGenres.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedGenres.Head()</MudLink>
|
|
@foreach (string genre in _sortedGenres.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"genre:""{genre.ToLowerInvariant()}""".GetRelativeSearchQuery())">@genre</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedTags.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Tags: </MudText>
|
|
<MudLink Href="@(@$"tag:""{_sortedTags.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedTags.Head()</MudLink>
|
|
@foreach (string tag in _sortedTags.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"tag:""{tag.ToLowerInvariant()}""".GetRelativeSearchQuery())">@tag</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudContainer>
|
|
<MudContainer MaxWidth="MaxWidth.Large">
|
|
<MudText Class="mb-4">Seasons</MudText>
|
|
</MudContainer>
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid">
|
|
@foreach (TelevisionSeasonCardViewModel card in _data.Cards)
|
|
{
|
|
<MediaCard Data="@card" Placeholder="@card.Placeholder"
|
|
Href="@($"media/tv/seasons/{card.TelevisionSeasonId}")"
|
|
AddToCollectionClicked="@AddSeasonToCollection"/>
|
|
}
|
|
</MudContainer>
|
|
@if ((_show?.Actors?.Count ?? 0) > 0)
|
|
{
|
|
<MudContainer MaxWidth="MaxWidth.Large">
|
|
<MudText Class="mb-4">Actors</MudText>
|
|
</MudContainer>
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid">
|
|
@foreach (ActorCardViewModel actor in _show.Actors)
|
|
{
|
|
<MediaCard Data="@actor"
|
|
Href="@(@$"actor:""{actor.Name.ToLowerInvariant()}""".GetRelativeSearchQuery())"
|
|
ArtworkKind="ArtworkKind.Thumbnail"/>
|
|
}
|
|
</MudContainer>
|
|
}
|
|
|
|
@code {
|
|
private readonly CancellationTokenSource _cts = new();
|
|
|
|
[Parameter]
|
|
public int ShowId { get; set; }
|
|
|
|
private TelevisionShowViewModel _show;
|
|
private List<string> _sortedContentRatings = [];
|
|
private List<CultureInfo> _sortedLanguages = [];
|
|
private List<string> _sortedStudios = [];
|
|
private List<string> _sortedNetworks = [];
|
|
private List<string> _sortedGenres = [];
|
|
private List<string> _sortedTags = [];
|
|
|
|
private int _pageSize => 100;
|
|
private readonly int _pageNumber = 1;
|
|
|
|
private TelevisionSeasonCardResultsViewModel _data = new(0, new List<TelevisionSeasonCardViewModel>(), null);
|
|
|
|
public void Dispose()
|
|
{
|
|
_cts.Cancel();
|
|
_cts.Dispose();
|
|
}
|
|
|
|
protected override Task OnParametersSetAsync() => RefreshData();
|
|
|
|
private async Task RefreshData()
|
|
{
|
|
Option<TelevisionShowViewModel> maybeShow = await Mediator.Send(new GetTelevisionShowById(ShowId), _cts.Token);
|
|
foreach (TelevisionShowViewModel show in maybeShow)
|
|
{
|
|
_show = show;
|
|
_sortedContentRatings = _show.ContentRatings.OrderBy(cr => cr).ToList();
|
|
_sortedLanguages = _show.Languages.OrderBy(ci => ci.EnglishName).ToList();
|
|
_sortedStudios = _show.Studios.OrderBy(s => s).ToList();
|
|
_sortedGenres = _show.Genres.OrderBy(g => g).ToList();
|
|
_sortedTags = _show.Tags.OrderBy(t => t).ToList();
|
|
_sortedNetworks = _show.Networks.OrderBy(n => n).ToList();
|
|
}
|
|
|
|
_data = await Mediator.Send(new GetTelevisionSeasonCards(ShowId, _pageNumber, _pageSize), _cts.Token);
|
|
}
|
|
|
|
private async Task AddToCollection()
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "show" }, { "EntityName", _show.Title } };
|
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
|
|
|
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
|
|
DialogResult result = await dialog.Result;
|
|
if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
|
|
{
|
|
await Mediator.Send(new AddShowToCollection(collection.Id, ShowId), _cts.Token);
|
|
NavigationManager.NavigateTo($"media/collections/{collection.Id}");
|
|
}
|
|
}
|
|
|
|
private async Task AddToPlaylist()
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "show" }, { "EntityName", _show.Title } };
|
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
|
|
|
IDialogReference dialog = await Dialog.ShowAsync<AddToPlaylistDialog>("Add To Playlist", parameters, options);
|
|
DialogResult result = await dialog.Result;
|
|
if (!result.Canceled && result.Data is PlaylistViewModel playlist)
|
|
{
|
|
await Mediator.Send(new AddShowToPlaylist(playlist.Id, ShowId), _cts.Token);
|
|
NavigationManager.NavigateTo($"media/playlists/{playlist.Id}");
|
|
}
|
|
}
|
|
|
|
private async Task AddToSchedule()
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "show" }, { "EntityName", _show.Title } };
|
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
|
|
|
IDialogReference dialog = await Dialog.ShowAsync<AddToScheduleDialog>("Add To Schedule", parameters, options);
|
|
DialogResult result = await dialog.Result;
|
|
if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule)
|
|
{
|
|
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
|
|
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
|
|
}
|
|
}
|
|
|
|
private async Task AddSeasonToCollection(MediaCardViewModel card)
|
|
{
|
|
if (card is TelevisionSeasonCardViewModel season)
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", season.Title } };
|
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
|
|
|
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
|
|
DialogResult result = await dialog.Result;
|
|
if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
|
|
{
|
|
var request = new AddSeasonToCollection(collection.Id, season.TelevisionSeasonId);
|
|
Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token);
|
|
addResult.Match(
|
|
Left: error =>
|
|
{
|
|
Snackbar.Add($"Unexpected error adding season to collection: {error.Value}");
|
|
Logger.LogError("Unexpected error adding season to collection: {Error}", error.Value);
|
|
},
|
|
Right: _ => Snackbar.Add($"Added {season.Title} to collection {collection.Name}", Severity.Success));
|
|
}
|
|
}
|
|
}
|
|
|
|
} |