Files
ersatztv/ErsatzTV/Pages/Artist.razor
T
Jason DoveandGitHub 7e30444857 dependencies and code cleanup (#2117)
* fix validation in new form layout

* pin mediatr to last oss version

* update dependencies

* cleanup code in core

* cleanup code in ffmpeg

* cleanup code in infra

* cleanup code in scanner

* cleanup code in application

* cleanup main code

* cleanup test code

* solution-wide code cleanup
2025-07-06 15:56:17 +00:00

287 lines
14 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.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"/>
}
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 200px">
<div style="display: flex; flex-direction: row;" class="mb-6">
@if (!string.IsNullOrWhiteSpace(_artist.Thumbnail))
{
<img class="mud-elevation-2 mr-6"
style="border-radius: 4px; flex-shrink: 0; height: 220px; width: 220px"
src="@($"artwork/thumbnails/{_artist.Thumbnail}")" alt="artist thumbnail"/>
}
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h2" Class="media-item-title">@_artist.Name</MudText>
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_artist.Disambiguation</MudText>
@if (!string.IsNullOrWhiteSpace(_artist.Biography))
{
<MudCard Elevation="2" Class="mb-6">
<MudCardContent Class="mx-3 my-3" Style="height: 100%">
<MudText Style="flex-grow: 1">
@if (_artist.Biography.Length > 400)
{
@(_artist.Biography.Substring(0, 400) + "...")
}
else
{
@_artist.Biography
}
</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.Schedule"
OnClick="@AddToSchedule">
Add To Schedule
</MudButton>
</div>
</div>
</div>
<MudCard Class="mb-6">
<MudCardContent>
@if (_sortedLanguages.Any())
{
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Languages:&nbsp;</MudText>
<MudLink Href="@(@$"language:""{_sortedLanguages.Head().EnglishName.ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedLanguages.Head().EnglishName</MudLink>
@foreach (CultureInfo language in _sortedLanguages.Skip(1))
{
<MudText>,&nbsp;</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:&nbsp;</MudText>
<MudLink Href="@(@$"genre:""{_sortedGenres.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedGenres.Head()</MudLink>
@foreach (string genre in _sortedGenres.Skip(1))
{
<MudText>,&nbsp;</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:&nbsp;</MudText>
<MudLink Href="@(@$"style:""{_sortedStyles.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedStyles.Head()</MudLink>
@foreach (string style in _sortedStyles.Skip(1))
{
<MudText>,&nbsp;</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:&nbsp;</MudText>
<MudLink Href="@(@$"mood:""{_sortedMoods.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedMoods.Head()</MudLink>
@foreach (string mood in _sortedMoods.Skip(1))
{
<MudText>,&nbsp;</MudText>
<MudLink Href="@(@$"mood:""{mood.ToLowerInvariant()}""".GetRelativeSearchQuery())">@mood</MudLink>
}
</div>
}
</MudCardContent>
</MudCard>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
@foreach (MusicVideoCardViewModel musicVideo in _musicVideos.Cards)
{
<MudCard Class="mb-6" Style="display: flex; flex-direction: column">
<div id="@($"music-video-{musicVideo.MusicVideoId}")" style="display: flex; flex-direction: row; scroll-margin-top: 85px">
@if (!string.IsNullOrWhiteSpace(musicVideo.Poster))
{
<MudPaper style="display: flex; flex-direction: column; position: relative">
<MudCardMedia Image="@($"artwork/thumbnails/{musicVideo.Poster}")" Style="flex-grow: 1; height: 220px; width: 293px;"/>
@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>
}
</MudPaper>
}
else
{
<div style="display: flex; height: 220px; position: relative; width: 293px;">
<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>
}
<MudCardContent Class="ml-3">
<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:&nbsp;</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>
</div>
@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:&nbsp;</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:&nbsp;</MudText>
<MudText>@musicVideo.LocalPath</MudText>
</div>
}
</MudCard>
}
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public int ArtistId { get; set; }
private ArtistViewModel _artist;
private List<CultureInfo> _sortedLanguages = new();
private List<string> _sortedGenres = new();
private List<string> _sortedStyles = new();
private List<string> _sortedMoods = new();
private MusicVideoCardResultsViewModel _musicVideos;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData()
{
await Mediator.Send(new GetArtistById(ArtistId), _cts.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), _cts.Token);
}
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.Canceled && result.Data is 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.Canceled && result.Data is ProgramScheduleViewModel schedule)
{
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, 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 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.Canceled && result.Data is 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));
}
}
}