* rename collection type * split collections into separate pages * add rerun collection types, migration, editor * add rerun to classic schedule items * rerun plumbing in classic playout builder * start to implement rerun enumerator * add scheduledAt to enumerator movenext * maintain rerun history in db * fix shuffle * fix rerun allowed playback orders * fix updating rerun collections * update changelog; fix editing * update changelog
340 lines
17 KiB
Plaintext
340 lines
17 KiB
Plaintext
@page "/media/music/artists/{ArtistId:int}"
|
|
@using System.Globalization
|
|
@using ErsatzTV.Application.Artists
|
|
@using ErsatzTV.Application.MediaCards
|
|
@using ErsatzTV.Application.MediaCollections
|
|
@using ErsatzTV.Application.ProgramSchedules
|
|
@using ErsatzTV.Core.Search
|
|
@using ErsatzTV.Extensions
|
|
@implements IDisposable
|
|
@inject IMediator Mediator
|
|
@inject IDialogService Dialog
|
|
@inject NavigationManager NavigationManager
|
|
@inject ILogger<Artist> Logger
|
|
@inject ISnackbar Snackbar
|
|
|
|
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
|
|
<div class="fanart-tint"></div>
|
|
@if (!string.IsNullOrWhiteSpace(_artist?.FanArt))
|
|
{
|
|
<img src="@($"artwork/fanart/{_artist.FanArt}")" alt="fan art"/>
|
|
}
|
|
else
|
|
{
|
|
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Animation="Animation.False"/>
|
|
}
|
|
</MudContainer>
|
|
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 100px" Class="z-10">
|
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Spacing="6">
|
|
@if (!string.IsNullOrWhiteSpace(_artist?.Thumbnail))
|
|
{
|
|
<MudImage Elevation="2" Src="@($"artwork/thumbnails/{_artist.Thumbnail}")" Class="rounded-lg z-10" Style="height: 220px; width: 220px; margin-left: auto; margin-right: auto;"/>
|
|
}
|
|
else
|
|
{
|
|
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Class="rounded-lg z-10" Animation="Animation.False" Height="220px" Width="220px" Style="margin-left: auto; margin-right: auto"/>
|
|
}
|
|
<div style="display: flex; flex-direction: column; height: 100%; width: 100%" class="z-10">
|
|
<MudStack Row="false">
|
|
<MudHidden Invert="true" Breakpoint="Breakpoint.SmAndDown">
|
|
<MudText Typo="Typo.h4" Class="media-item-title">@_artist?.Name</MudText>
|
|
</MudHidden>
|
|
<MudHidden Invert="true" Breakpoint="Breakpoint.MdAndUp">
|
|
<MudText Typo="Typo.h2" Class="media-item-title">@_artist?.Name</MudText>
|
|
</MudHidden>
|
|
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_artist?.Disambiguation</MudText>
|
|
</MudStack>
|
|
<MudCard Elevation="2" Class="mb-6">
|
|
<MudCardContent Class="mx-3 my-3" Style="height: 100%">
|
|
@if (!string.IsNullOrWhiteSpace(_artist?.Biography))
|
|
{
|
|
<MudText Style="flex-grow: 1">
|
|
@if (_artist.Biography.Length > 400)
|
|
{
|
|
@(_artist.Biography.Substring(0, 400) + "...")
|
|
}
|
|
else
|
|
{
|
|
@_artist.Biography
|
|
}
|
|
</MudText>
|
|
}
|
|
else
|
|
{
|
|
<div style="height: 130px; flex-grow: 1">
|
|
</div>
|
|
}
|
|
</MudCardContent>
|
|
</MudCard>
|
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="mb-6">
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Add"
|
|
OnClick="@AddToCollection">
|
|
Add To Collection
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Schedule"
|
|
OnClick="@AddToSchedule">
|
|
Add To Schedule
|
|
</MudButton>
|
|
</MudStack>
|
|
</div>
|
|
</MudStack>
|
|
<MudCard Class="mb-6">
|
|
<MudCardContent>
|
|
@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 (_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 (_sortedStyles.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Styles: </MudText>
|
|
<MudLink Href="@(@$"style:""{_sortedStyles.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedStyles.Head()</MudLink>
|
|
@foreach (string style in _sortedStyles.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"style:""{style.ToLowerInvariant()}""".GetRelativeSearchQuery())">@style</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedMoods.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Moods: </MudText>
|
|
<MudLink Href="@(@$"mood:""{_sortedMoods.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedMoods.Head()</MudLink>
|
|
@foreach (string mood in _sortedMoods.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"mood:""{mood.ToLowerInvariant()}""".GetRelativeSearchQuery())">@mood</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudContainer>
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
|
<MudStack Row="false" Spacing="6">
|
|
@foreach (MusicVideoCardViewModel musicVideo in _musicVideos.Cards)
|
|
{
|
|
<MudCard>
|
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" id="@($"music-video-{musicVideo.MusicVideoId}")">
|
|
@if (!string.IsNullOrWhiteSpace(musicVideo.Poster))
|
|
{
|
|
<div style="display: flex; flex-direction: column; position: relative">
|
|
<MudImage Src="@($"artwork/thumbnails/{musicVideo.Poster}")" Style="height: 220px; max-width: 265px; margin-left: auto; margin-right: auto;"/>
|
|
@if (musicVideo.State == MediaItemState.FileNotFound)
|
|
{
|
|
<div style="position: absolute; right: 10px; top: 8px;">
|
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
|
|
</div>
|
|
}
|
|
else if (musicVideo.State == MediaItemState.Unavailable)
|
|
{
|
|
<div style="position: absolute; right: 10px; top: 8px;">
|
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large"/>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<MudHidden Invert="true" Breakpoint="Breakpoint.SmAndDown">
|
|
@if (musicVideo.State is not MediaItemState.Normal and not MediaItemState.RemoteOnly)
|
|
{
|
|
<div style="display: flex; position: relative; height: 220px; width: 265px; ">
|
|
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Animation="Animation.False"/>
|
|
@if (musicVideo.State == MediaItemState.FileNotFound)
|
|
{
|
|
<div style="position: absolute; right: 10px; top: 8px;">
|
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
|
|
</div>
|
|
}
|
|
else if (musicVideo.State == MediaItemState.Unavailable)
|
|
{
|
|
<div style="position: absolute; right: 10px; top: 8px;">
|
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large"/>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</MudHidden>
|
|
<MudHidden Invert="true" Breakpoint="Breakpoint.MdAndUp">
|
|
<div style="display: flex; position: relative; height: 220px; width: 265px; ">
|
|
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Animation="Animation.False"/>
|
|
@if (musicVideo.State == MediaItemState.FileNotFound)
|
|
{
|
|
<div style="position: absolute; right: 10px; top: 8px;">
|
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
|
|
</div>
|
|
}
|
|
else if (musicVideo.State == MediaItemState.Unavailable)
|
|
{
|
|
<div style="position: absolute; right: 10px; top: 8px;">
|
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large"/>
|
|
</div>
|
|
}
|
|
</div>
|
|
</MudHidden>
|
|
}
|
|
<MudCardContent>
|
|
<div style="display: flex; flex-direction: column; height: 100%">
|
|
<MudText Typo="Typo.h4">@musicVideo.Title</MudText>
|
|
@if (!string.IsNullOrWhiteSpace(musicVideo.Album))
|
|
{
|
|
<div style="display: flex; flex-direction: row">
|
|
<MudText GutterBottom="true">Album: </MudText>
|
|
<MudLink Href="@(@$"album:""{musicVideo.Album}""".GetRelativeSearchQuery())">@musicVideo.Album</MudLink>
|
|
</div>
|
|
}
|
|
<MudText Style="flex-grow: 1">@musicVideo.Plot</MudText>
|
|
<div class="mt-6">
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Add"
|
|
OnClick="@(_ => AddMusicVideoToCollection(musicVideo))">
|
|
Add To Collection
|
|
</MudButton>
|
|
</div>
|
|
</div>
|
|
</MudCardContent>
|
|
</MudStack>
|
|
@if (musicVideo.State == MediaItemState.FileNotFound)
|
|
{
|
|
<div class="ml-3 mt-3 mb-3" style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Class="mr-2"/>
|
|
<MudText>File Not Found: </MudText>
|
|
<MudText>@musicVideo.Path</MudText>
|
|
</div>
|
|
}
|
|
else if (musicVideo.State == MediaItemState.Unavailable)
|
|
{
|
|
<div class="ml-3 mt-3 mb-3" style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Class="mr-2"/>
|
|
<MudText>Unavailable: </MudText>
|
|
<MudText>@musicVideo.LocalPath</MudText>
|
|
</div>
|
|
}
|
|
</MudCard>
|
|
}
|
|
</MudStack>
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private CancellationTokenSource _cts;
|
|
|
|
[Parameter]
|
|
public int ArtistId { get; set; }
|
|
|
|
private ArtistViewModel _artist;
|
|
private List<CultureInfo> _sortedLanguages = [];
|
|
private List<string> _sortedGenres = [];
|
|
private List<string> _sortedStyles = [];
|
|
private List<string> _sortedMoods = [];
|
|
private MusicVideoCardResultsViewModel _musicVideos = new(0, [], new SearchPageMap([]));
|
|
|
|
public void Dispose()
|
|
{
|
|
_cts?.Cancel();
|
|
_cts?.Dispose();
|
|
}
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
_cts?.Cancel();
|
|
_cts?.Dispose();
|
|
_cts = new CancellationTokenSource();
|
|
var token = _cts.Token;
|
|
|
|
try
|
|
{
|
|
await Mediator.Send(new GetArtistById(ArtistId), token).IfSomeAsync(vm =>
|
|
{
|
|
_artist = vm;
|
|
_sortedLanguages = _artist.Languages.OrderBy(ci => ci.EnglishName).ToList();
|
|
_sortedGenres = _artist.Genres.OrderBy(g => g).ToList();
|
|
_sortedStyles = _artist.Styles.OrderBy(s => s).ToList();
|
|
_sortedMoods = _artist.Moods.OrderBy(m => m).ToList();
|
|
});
|
|
|
|
_musicVideos = await Mediator.Send(new GetMusicVideoCards(ArtistId, 1, 100), token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// do nothing
|
|
}
|
|
}
|
|
|
|
private async Task AddToCollection()
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "artist" }, { "EntityName", _artist.Name } };
|
|
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 is { Canceled: false, Data: MediaCollectionViewModel collection })
|
|
{
|
|
await Mediator.Send(new AddArtistToCollection(collection.Id, ArtistId), _cts.Token);
|
|
NavigationManager.NavigateTo($"media/collections/{collection.Id}");
|
|
}
|
|
}
|
|
|
|
private async Task AddToSchedule()
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "artist" }, { "EntityName", _artist.Name } };
|
|
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 is { Canceled: false, Data: ProgramScheduleViewModel schedule })
|
|
{
|
|
await Mediator.Send(AddProgramScheduleItem.ForMediaItem(schedule.Id, CollectionType.Artist, ArtistId), _cts.Token);
|
|
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
|
|
}
|
|
}
|
|
|
|
private async Task AddMusicVideoToCollection(MusicVideoCardViewModel musicVideo)
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "music video" }, { "EntityName", musicVideo.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 is { Canceled: false, Data: MediaCollectionViewModel collection })
|
|
{
|
|
var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId);
|
|
Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token);
|
|
addResult.Match(
|
|
Left: error =>
|
|
{
|
|
Snackbar.Add($"Unexpected error adding music video to collection: {error.Value}");
|
|
Logger.LogError("Unexpected error adding music video to collection: {Error}", error.Value);
|
|
},
|
|
Right: _ => Snackbar.Add($"Added {musicVideo.Title} to collection {collection.Name}", Severity.Success));
|
|
}
|
|
}
|
|
|
|
}
|