add trash system for local libraries (#571)
* flag local movies as file not found * show warning icon on cards * unflag movie that is found during scan * skip missing files when building playouts * add state to search index * add file not found health check * link to search from file not found health check * support flagging other media kinds as file not found * continue to schedule missing items * support episode files not found * wip trash page * fix trash url * trash page is functional * update changelog * fix changelog merge
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<NoWarn>VSTHRD200</NoWarn>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -124,17 +124,31 @@
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
||||
@foreach (MusicVideoCardViewModel musicVideo in _musicVideos.Cards)
|
||||
{
|
||||
<MudCard Class="mb-6">
|
||||
<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">
|
||||
<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; top: 8px; right: 10px">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
|
||||
</div>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Animation="Animation.False" Height="220px" Width="293px"/>
|
||||
<div style="height: 220px; width: 293px; display: flex; position: relative">
|
||||
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Animation="Animation.False"/>
|
||||
@if (musicVideo.State == MediaItemState.FileNotFound)
|
||||
{
|
||||
<div style="position: absolute; top: 8px; right: 10px">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<MudCardContent Class="ml-3">
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
@@ -158,6 +172,14 @@
|
||||
</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: </MudText>
|
||||
<MudText>@musicVideo.Path</MudText>
|
||||
</div>
|
||||
}
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@@ -49,7 +49,23 @@
|
||||
<div class="ml-2">@context.Title</div>
|
||||
</div>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Message">@context.Message</MudTd>
|
||||
<MudTd DataLabel="Message">
|
||||
@if (context.Link.IsSome)
|
||||
{
|
||||
foreach (string link in context.Link)
|
||||
{
|
||||
<MudLink Href="@link">
|
||||
@context.Message
|
||||
</MudLink>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>
|
||||
@context.Message
|
||||
</MudText>
|
||||
}
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudContainer>
|
||||
|
||||
+32
-12
@@ -28,18 +28,26 @@
|
||||
<div style="display: flex; flex-direction: row;" class="mb-6">
|
||||
@if (!string.IsNullOrWhiteSpace(_movie.Poster))
|
||||
{
|
||||
if (_movie.Poster.StartsWith("http://") || _movie.Poster.StartsWith("https://"))
|
||||
{
|
||||
<img class="mud-elevation-2 mr-6"
|
||||
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
|
||||
src="@_movie.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/{_movie.Poster}")" alt="movie poster"/>
|
||||
}
|
||||
<div class="mr-6" style="display: flex; flex-direction: column; max-height: 440px; position: relative">
|
||||
@if (_movie.Poster.StartsWith("http://") || _movie.Poster.StartsWith("https://"))
|
||||
{
|
||||
<img class="mud-elevation-2"
|
||||
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
|
||||
src="@_movie.Poster" alt="movie poster"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<img class="mud-elevation-2"
|
||||
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
|
||||
src="@($"/artwork/posters/{_movie.Poster}")" alt="movie poster"/>
|
||||
}
|
||||
@if (_movie.MediaItemState == MediaItemState.FileNotFound)
|
||||
{
|
||||
<div style="position: absolute; top: 8px; right: 10px">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
<MudText Typo="Typo.h2" Class="media-item-title">@_movie.Title</MudText>
|
||||
@@ -62,6 +70,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (_movie.MediaItemState == MediaItemState.FileNotFound)
|
||||
{
|
||||
<MudCard Class="mb-6">
|
||||
<MudCardContent>
|
||||
<div 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>@_movie.Path</MudText>
|
||||
</div>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
<MudCard Class="mb-6">
|
||||
<MudCardContent>
|
||||
@if (_sortedContentRatings.Any())
|
||||
|
||||
@@ -42,6 +42,8 @@ namespace ErsatzTV.Pages
|
||||
[Inject]
|
||||
protected IMediator Mediator { get; set; }
|
||||
|
||||
protected System.Collections.Generic.HashSet<MediaCardViewModel> SelectedItems => _selectedItems;
|
||||
|
||||
protected bool IsSelected(MediaCardViewModel card) =>
|
||||
_selectedItems.Contains(card);
|
||||
|
||||
|
||||
@@ -78,18 +78,22 @@
|
||||
<div id="@($"episode-{episode.EpisodeId}")" style="display: flex; flex-direction: row; scroll-margin-top: 85px">
|
||||
@if (!string.IsNullOrWhiteSpace(episode.Poster))
|
||||
{
|
||||
if (episode.Poster.StartsWith("http://") || episode.Poster.StartsWith("https://"))
|
||||
{
|
||||
<MudPaper style="display: flex; flex-direction: column">
|
||||
<MudPaper style="display: flex; flex-direction: column; position: relative">
|
||||
@if (episode.Poster.StartsWith("http://") || episode.Poster.StartsWith("https://"))
|
||||
{
|
||||
<MudCardMedia Image="@episode.Poster" Style="flex-grow: 1; height: 220px; width: 392px;"/>
|
||||
</MudPaper>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper style="display: flex; flex-direction: column">
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudCardMedia Image="@($"/artwork/thumbnails/{episode.Poster}")" Style="flex-grow: 1; height: 220px; width: 392px;"/>
|
||||
</MudPaper>
|
||||
}
|
||||
}
|
||||
@if (episode.State == MediaItemState.FileNotFound)
|
||||
{
|
||||
<div style="position: absolute; top: 8px; right: 10px">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
|
||||
</div>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
<MudCardContent Class="ml-3">
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
@@ -107,6 +111,14 @@
|
||||
</MudCardContent>
|
||||
</div>
|
||||
<div class="pl-3 pt-3">
|
||||
@if (episode.State == MediaItemState.FileNotFound)
|
||||
{
|
||||
<div class="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>@episode.Path</MudText>
|
||||
</div>
|
||||
}
|
||||
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
||||
<MudText GutterBottom="true">Released: @episode.Aired.ToShortDateString()</MudText>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,564 @@
|
||||
@page "/media/trash"
|
||||
@using ErsatzTV.Application.MediaCards
|
||||
@using ErsatzTV.Application.Search.Queries
|
||||
@using ErsatzTV.Extensions
|
||||
@using Unit = LanguageExt.Unit
|
||||
@using ErsatzTV.Application.Maintenance.Commands
|
||||
@inherits MultiSelectBase<Search>
|
||||
@inject NavigationManager _navigationManager
|
||||
@inject ChannelWriter<IBackgroundServiceRequest> _channel
|
||||
|
||||
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;">
|
||||
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
|
||||
@if (IsSelectMode())
|
||||
{
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
|
||||
<div style="margin-left: auto">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Error"
|
||||
StartIcon="@Icons.Material.Filled.Delete"
|
||||
OnClick="@(_ => DeleteFromDatabase())">
|
||||
Delete From Database
|
||||
</MudButton>
|
||||
<MudButton Class="ml-3"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.Check"
|
||||
OnClick="@(_ => ClearSelection())">
|
||||
Clear Selection
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_movies?.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#movies")" Style="margin-bottom: auto; margin-top: auto">@_movies.Count Movies</MudLink>
|
||||
}
|
||||
|
||||
if (_shows?.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#shows")" Style="margin-bottom: auto; margin-top: auto">@_shows.Count Shows</MudLink>
|
||||
}
|
||||
|
||||
if (_seasons?.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#seasons")" Style="margin-bottom: auto; margin-top: auto">@_seasons.Count Seasons</MudLink>
|
||||
}
|
||||
|
||||
if (_episodes?.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#episodes")" Style="margin-bottom: auto; margin-top: auto">@_episodes.Count Episodes</MudLink>
|
||||
}
|
||||
|
||||
if (_artists?.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#artists")" Style="margin-bottom: auto; margin-top: auto">@_artists.Count Artists</MudLink>
|
||||
}
|
||||
|
||||
if (_musicVideos?.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#music_videos")" Style="margin-bottom: auto; margin-top: auto">@_musicVideos.Count Music Videos</MudLink>
|
||||
}
|
||||
|
||||
if (_otherVideos?.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#other_videos")" Style="margin-bottom: auto; margin-top: auto">@_otherVideos.Count Other Videos</MudLink>
|
||||
}
|
||||
|
||||
if (_songs?.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#songs")" Style="margin-bottom: auto; margin-top: auto">@_songs.Count Songs</MudLink>
|
||||
}
|
||||
|
||||
if (_movies?.Count == 0 && _shows?.Count == 0 && _seasons?.Count == 0 && _episodes?.Count == 0 && _artists?.Count == 0 && _musicVideos?.Count == 0 && _otherVideos?.Count == 0 && _songs?.Count == 0)
|
||||
{
|
||||
<MudText>Nothing to see here...</MudText>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</MudPaper>
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Style="margin-top: 96px">
|
||||
@if (_movies?.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
|
||||
Movies
|
||||
</MudText>
|
||||
@if (_movies.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetMoviesLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (MovieCardViewModel card in _movies.Cards.OrderBy(m => m.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link="@($"/media/movies/{card.MovieId}")"
|
||||
DeleteClicked="@DeleteItemFromDatabase"
|
||||
SelectColor="@Color.Error"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_shows?.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
|
||||
Shows
|
||||
</MudText>
|
||||
@if (_shows.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetShowsLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (TelevisionShowCardViewModel card in _shows.Cards.OrderBy(s => s.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link="@($"/media/tv/shows/{card.TelevisionShowId}")"
|
||||
DeleteClicked="@DeleteItemFromDatabase"
|
||||
SelectColor="@Color.Error"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_seasons?.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
|
||||
Seasons
|
||||
</MudText>
|
||||
@if (_seasons.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetSeasonsLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (TelevisionSeasonCardViewModel card in _seasons.Cards.OrderBy(s => s.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")"
|
||||
DeleteClicked="@DeleteItemFromDatabase"
|
||||
SelectColor="@Color.Error"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_episodes?.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
|
||||
Episodes
|
||||
</MudText>
|
||||
@if (_episodes.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetEpisodesLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
DeleteClicked="@DeleteItemFromDatabase"
|
||||
SelectColor="@Color.Error"
|
||||
Link="@($"/media/tv/seasons/{card.SeasonId}#episode-{card.EpisodeId}")"
|
||||
Subtitle="@($"{card.ShowTitle} - S{card.Season} E{card.Episode}")"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_artists?.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
|
||||
Artists
|
||||
</MudText>
|
||||
@if (_artists.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetArtistsLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (ArtistCardViewModel card in _artists.Cards.OrderBy(s => s.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link="@($"/media/music/artists/{card.ArtistId}")"
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
DeleteClicked="@DeleteItemFromDatabase"
|
||||
SelectColor="@Color.Error"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_musicVideos?.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
|
||||
Music Videos
|
||||
</MudText>
|
||||
@if (_musicVideos.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetMusicVideosLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link=""
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
DeleteClicked="@DeleteItemFromDatabase"
|
||||
SelectColor="@Color.Error"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_otherVideos?.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
|
||||
Other Videos
|
||||
</MudText>
|
||||
@if (_otherVideos.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetOtherVideosLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (OtherVideoCardViewModel card in _otherVideos.Cards.OrderBy(s => s.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link=""
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
DeleteClicked="@DeleteItemFromDatabase"
|
||||
SelectColor="@Color.Error"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_songs?.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
|
||||
Songs
|
||||
</MudText>
|
||||
@if (_songs.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetSongsLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (SongCardViewModel card in _songs.Cards.OrderBy(s => s.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link=""
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
DeleteClicked="@DeleteItemFromDatabase"
|
||||
SelectColor="@Color.Error"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private string _query;
|
||||
private MovieCardResultsViewModel _movies;
|
||||
private TelevisionShowCardResultsViewModel _shows;
|
||||
private TelevisionSeasonCardResultsViewModel _seasons;
|
||||
private TelevisionEpisodeCardResultsViewModel _episodes;
|
||||
private MusicVideoCardResultsViewModel _musicVideos;
|
||||
private OtherVideoCardResultsViewModel _otherVideos;
|
||||
private SongCardResultsViewModel _songs;
|
||||
private ArtistCardResultsViewModel _artists;
|
||||
|
||||
protected override Task OnInitializedAsync() => RefreshData();
|
||||
|
||||
protected override async Task RefreshData()
|
||||
{
|
||||
_query = "state:FileNotFound";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50));
|
||||
_shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50));
|
||||
_seasons = await Mediator.Send(new QuerySearchIndexSeasons($"type:season AND ({_query})", 1, 50));
|
||||
_episodes = await Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50));
|
||||
_musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50));
|
||||
_otherVideos = await Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50));
|
||||
_songs = await Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50));
|
||||
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50));
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
||||
{
|
||||
List<MediaCardViewModel> GetSortedItems()
|
||||
{
|
||||
return _movies.Cards.OrderBy(m => m.SortTitle)
|
||||
.Append<MediaCardViewModel>(_shows.Cards.OrderBy(s => s.SortTitle))
|
||||
.Append(_seasons.Cards.OrderBy(s => s.SortTitle))
|
||||
.Append(_episodes.Cards.OrderBy(ep => ep.SortTitle))
|
||||
.Append(_artists.Cards.OrderBy(a => a.SortTitle))
|
||||
.Append(_musicVideos.Cards.OrderBy(mv => mv.SortTitle))
|
||||
.Append(_otherVideos.Cards.OrderBy(ov => ov.SortTitle))
|
||||
.Append(_songs.Cards.OrderBy(ov => ov.SortTitle))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
SelectClicked(GetSortedItems, card, e);
|
||||
}
|
||||
|
||||
private string GetMoviesLink()
|
||||
{
|
||||
var uri = "/media/movies/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GetShowsLink()
|
||||
{
|
||||
var uri = "/media/tv/shows/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GetSeasonsLink()
|
||||
{
|
||||
var uri = "/media/tv/seasons/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GetEpisodesLink()
|
||||
{
|
||||
var uri = "/media/tv/episodes/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GetArtistsLink()
|
||||
{
|
||||
var uri = "/media/music/artists/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GetMusicVideosLink()
|
||||
{
|
||||
var uri = "/media/music/videos/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GetOtherVideosLink()
|
||||
{
|
||||
var uri = "/media/other/videos/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GetSongsLink()
|
||||
{
|
||||
var uri = "/media/music/songs/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private Task DeleteFromDatabase() => DeleteItemsFromDatabase(
|
||||
SelectedItems.OfType<MovieCardViewModel>().Map(m => m.MovieId).ToList(),
|
||||
SelectedItems.OfType<TelevisionShowCardViewModel>().Map(s => s.TelevisionShowId).ToList(),
|
||||
SelectedItems.OfType<TelevisionSeasonCardViewModel>().Map(s => s.TelevisionSeasonId).ToList(),
|
||||
SelectedItems.OfType<TelevisionEpisodeCardViewModel>().Map(e => e.EpisodeId).ToList(),
|
||||
SelectedItems.OfType<ArtistCardViewModel>().Map(a => a.ArtistId).ToList(),
|
||||
SelectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId).ToList(),
|
||||
SelectedItems.OfType<OtherVideoCardViewModel>().Map(ov => ov.OtherVideoId).ToList(),
|
||||
SelectedItems.OfType<SongCardViewModel>().Map(s => s.SongId).ToList());
|
||||
|
||||
private async Task DeleteItemsFromDatabase(
|
||||
List<int> movieIds,
|
||||
List<int> showIds,
|
||||
List<int> seasonIds,
|
||||
List<int> episodeIds,
|
||||
List<int> artistIds,
|
||||
List<int> musicVideoIds,
|
||||
List<int> otherVideoIds,
|
||||
List<int> songIds,
|
||||
string entityName = "selected items")
|
||||
{
|
||||
int count = movieIds.Count + showIds.Count + seasonIds.Count + episodeIds.Count + artistIds.Count +
|
||||
musicVideoIds.Count + otherVideoIds.Count + songIds.Count;
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{ { "EntityType", count.ToString() }, { "EntityName", entityName } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = Dialog.Show<DeleteFromDatabaseDialog>("Delete From Database", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled)
|
||||
{
|
||||
var request = new DeleteItemsFromDatabase(
|
||||
movieIds.Append(showIds)
|
||||
.Append(seasonIds)
|
||||
.Append(episodeIds)
|
||||
.Append(artistIds)
|
||||
.Append(musicVideoIds)
|
||||
.Append(otherVideoIds)
|
||||
.Append(songIds)
|
||||
.ToList());
|
||||
|
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||
await addResult.Match(
|
||||
Left: error =>
|
||||
{
|
||||
Snackbar.Add($"Unexpected error deleting items from database: {error.Value}");
|
||||
Logger.LogError("Unexpected error deleting items from database: {Error}", error.Value);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
Right: async _ =>
|
||||
{
|
||||
Snackbar.Add($"Deleted {count} items from the database", Severity.Success);
|
||||
ClearSelection();
|
||||
await RefreshData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteItemFromDatabase(MediaCardViewModel vm)
|
||||
{
|
||||
DeleteItemsFromDatabase request;
|
||||
|
||||
switch (vm)
|
||||
{
|
||||
case MovieCardViewModel movie:
|
||||
request = new DeleteItemsFromDatabase(new List<int> { movie.MovieId });
|
||||
await DeleteItemsWithConfirmation("movie", $"{movie.Title} ({movie.Subtitle})", request);
|
||||
break;
|
||||
case TelevisionShowCardViewModel show:
|
||||
request = new DeleteItemsFromDatabase(new List<int> { show.TelevisionShowId });
|
||||
await DeleteItemsWithConfirmation("show", $"{show.Title} ({show.Subtitle})", request);
|
||||
break;
|
||||
case TelevisionSeasonCardViewModel season:
|
||||
request = new DeleteItemsFromDatabase(new List<int> { season.TelevisionSeasonId });
|
||||
await DeleteItemsWithConfirmation("season", $"{season.Title} ({season.Subtitle})", request);
|
||||
break;
|
||||
case TelevisionEpisodeCardViewModel episode:
|
||||
request = new DeleteItemsFromDatabase(new List<int> { episode.EpisodeId });
|
||||
await DeleteItemsWithConfirmation("episode", $"{episode.Title} ({episode.Subtitle})", request);
|
||||
break;
|
||||
case ArtistCardViewModel artist:
|
||||
request = new DeleteItemsFromDatabase(new List<int> { artist.ArtistId });
|
||||
await DeleteItemsWithConfirmation("artist", $"{artist.Title} ({artist.Subtitle})", request);
|
||||
break;
|
||||
case MusicVideoCardViewModel musicVideo:
|
||||
request = new DeleteItemsFromDatabase(new List<int> { musicVideo.MusicVideoId });
|
||||
await DeleteItemsWithConfirmation("music video", $"{musicVideo.Title} ({musicVideo.Subtitle})", request);
|
||||
break;
|
||||
case OtherVideoCardViewModel otherVideo:
|
||||
request = new DeleteItemsFromDatabase(new List<int> { otherVideo.OtherVideoId });
|
||||
await DeleteItemsWithConfirmation("other video", $"{otherVideo.Title} ({otherVideo.Subtitle})", request);
|
||||
break;
|
||||
case SongCardViewModel song:
|
||||
request = new DeleteItemsFromDatabase(new List<int> { song.SongId });
|
||||
await DeleteItemsWithConfirmation("song", $"{song.Title} ({song.Subtitle})", request);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteItemsWithConfirmation(
|
||||
string entityType,
|
||||
string entityName,
|
||||
DeleteItemsFromDatabase request)
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", entityType }, { "EntityName", entityName } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = Dialog.Show<DeleteFromDatabaseDialog>("Delete From Database", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled)
|
||||
{
|
||||
await Mediator.Send(request);
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
[Parameter]
|
||||
public string DetailHighlight { get; set; }
|
||||
|
||||
private readonly MediaCollectionViewModel _newCollection = new(-1, "(New Collection)", false);
|
||||
private readonly MediaCollectionViewModel _newCollection = new(-1, "(New Collection)", false, MediaItemState.Normal);
|
||||
private string _newCollectionName;
|
||||
|
||||
private List<MediaCollectionViewModel> _collections;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
@inject IMediator _mediator
|
||||
|
||||
<div @onkeydown="@OnKeyDown">
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudContainer Class="mb-6">
|
||||
<MudHighlighter Class="mud-primary-text"
|
||||
Style="background-color: transparent; font-weight: bold"
|
||||
Text="@FormatText()"
|
||||
HighlightedText="@EntityName"/>
|
||||
</MudContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||
<MudButton Color="Color.Error" Variant="Variant.Filled" OnClick="Submit">
|
||||
Delete From Database
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[CascadingParameter]
|
||||
MudDialogInstance MudDialog { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string EntityType { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string EntityName { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string DetailText { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string DetailHighlight { get; set; }
|
||||
|
||||
private string FormatText() => $"Do you really want to delete the {EntityType} {EntityName} from the database?";
|
||||
|
||||
private void Submit() => MudDialog.Close(DialogResult.Ok(true));
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private void OnKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Code is "Enter" or "NumpadEnter")
|
||||
{
|
||||
Submit();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,7 @@
|
||||
</MudNavGroup>
|
||||
<MudNavGroup Title="Media" Expanded="true">
|
||||
<MudNavLink Href="/media/libraries">Libraries</MudNavLink>
|
||||
<MudNavLink Href="/media/trash">Trash</MudNavLink>
|
||||
<MudNavLink Href="/media/tv/shows">TV Shows</MudNavLink>
|
||||
<MudNavLink Href="/media/movies">Movies</MudNavLink>
|
||||
<MudNavLink Href="/media/music/artists">Music</MudNavLink>
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
Style="margin: auto"/>
|
||||
</div>
|
||||
}
|
||||
@if (Data.State == MediaItemState.FileNotFound)
|
||||
{
|
||||
<div style="position: absolute; top: 12px; right: 12px">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error"/>
|
||||
</div>
|
||||
}
|
||||
<div class="media-card-overlay" style="">
|
||||
<MudButton Link="@(IsSelectMode ? null : Link)"
|
||||
Style="height: 100%; width: 100%"
|
||||
|
||||
@@ -266,6 +266,7 @@ namespace ErsatzTV
|
||||
services.AddScoped<IMovieMetadataHealthCheck, MovieMetadataHealthCheck>();
|
||||
services.AddScoped<IEpisodeMetadataHealthCheck, EpisodeMetadataHealthCheck>();
|
||||
services.AddScoped<IZeroDurationHealthCheck, ZeroDurationHealthCheck>();
|
||||
services.AddScoped<IFileNotFoundHealthCheck, FileNotFoundHealthCheck>();
|
||||
services.AddScoped<IVaapiDriverHealthCheck, VaapiDriverHealthCheck>();
|
||||
services.AddScoped<IHealthCheckService, HealthCheckService>();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Application.MediaCollections;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.ViewModels
|
||||
{
|
||||
@@ -16,7 +17,8 @@ namespace ErsatzTV.ViewModels
|
||||
Collection = new MediaCollectionViewModel(
|
||||
_smartCollection.Id,
|
||||
_smartCollection.Name,
|
||||
false);
|
||||
false,
|
||||
MediaItemState.Normal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user