@page "/media/trash" @using ErsatzTV.Application.Maintenance @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.Search @using ErsatzTV.Core.Search @using ErsatzTV.Extensions @inherits MultiSelectBase @inject NavigationManager NavigationManager @inject PersistentComponentState ApplicationState
@if (IsSelectMode()) {
@SelectionLabel()
Delete From Database Clear Selection
} else if (IsNotEmpty) {
@if (_movies?.Cards.Count > 0) { @_movies.Count Movies } @if (_shows?.Cards.Count > 0) { @_shows.Count Shows } @if (_seasons?.Cards.Count > 0) { @_seasons.Count Seasons } @if (_episodes?.Cards.Count > 0) { @_episodes.Count Episodes } @if (_artists?.Cards.Count > 0) { @_artists.Count Artists } @if (_musicVideos?.Cards.Count > 0) { @_musicVideos.Count Music Videos } @if (_otherVideos?.Cards.Count > 0) { @_otherVideos.Count Other Videos } @if (_songs?.Cards.Count > 0) { @_songs.Count Songs } @if (_images?.Cards.Count > 0) { @_images.Count Images } @if (_remoteStreams?.Cards.Count > 0) { @_remoteStreams.Count Remote Streams }
Select All Empty Trash
Select All Empty Trash
} else { Nothing to see here... }
@if (_movies?.Cards.Count > 0) {
Movies @if (_movies.Count > 50) { See All >> }
@foreach (MovieCardViewModel card in _movies.Cards.OrderBy(m => m.SortTitle)) { } } @if (_shows?.Cards.Count > 0) {
Shows @if (_shows.Count > 50) { See All >> }
@foreach (TelevisionShowCardViewModel card in _shows.Cards.OrderBy(s => s.SortTitle)) { } } @if (_seasons?.Cards.Count > 0) {
Seasons @if (_seasons.Count > 50) { See All >> }
@foreach (TelevisionSeasonCardViewModel card in _seasons.Cards.OrderBy(s => s.SortTitle)) { } } @if (_episodes?.Cards.Count > 0) {
Episodes @if (_episodes.Count > 50) { See All >> }
@foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle)) { } } @if (_artists?.Cards.Count > 0) {
Artists @if (_artists.Count > 50) { See All >> }
@foreach (ArtistCardViewModel card in _artists.Cards.OrderBy(s => s.SortTitle)) { } } @if (_musicVideos?.Cards.Count > 0) {
Music Videos @if (_musicVideos.Count > 50) { See All >> }
@foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle)) { } } @if (_otherVideos?.Cards.Count > 0) {
Other Videos @if (_otherVideos.Count > 50) { See All >> }
@foreach (OtherVideoCardViewModel card in _otherVideos.Cards.OrderBy(s => s.SortTitle)) { } } @if (_songs?.Cards.Count > 0) {
Songs @if (_songs.Count > 50) { See All >> }
@foreach (SongCardViewModel card in _songs.Cards.OrderBy(s => s.SortTitle)) { } } @if (_images?.Cards.Count > 0) {
Images @if (_images.Count > 50) { See All >> }
@foreach (ImageCardViewModel card in _images.Cards.OrderBy(s => s.SortTitle)) { } } @if (_remoteStreams?.Cards.Count > 0) {
Remote Streams @if (_remoteStreams.Count > 50) { See All >> }
@foreach (RemoteStreamCardViewModel card in _remoteStreams.Cards.OrderBy(s => s.SortTitle)) { } }
@code { private CancellationTokenSource _cts; private string _query; private MovieCardResultsViewModel _movies = new(0, [], new SearchPageMap([])); private TelevisionShowCardResultsViewModel _shows = new(0, [], new SearchPageMap([])); private TelevisionSeasonCardResultsViewModel _seasons = new(0, [], new SearchPageMap([])); private TelevisionEpisodeCardResultsViewModel _episodes = new(0, [], new SearchPageMap([])); private MusicVideoCardResultsViewModel _musicVideos = new(0, [], new SearchPageMap([])); private OtherVideoCardResultsViewModel _otherVideos = new(0, [], new SearchPageMap([])); private SongCardResultsViewModel _songs = new(0, [], new SearchPageMap([])); private ImageCardResultsViewModel _images = new(0, [], new SearchPageMap([])); private RemoteStreamCardResultsViewModel _remoteStreams = new(0, [], new SearchPageMap([])); private ArtistCardResultsViewModel _artists = new(0, [], new SearchPageMap([])); private PersistingComponentStateSubscription _persistingSubscription; protected override void Dispose(bool disposing) { if (disposing) { _persistingSubscription.Dispose(); _cts?.Cancel(); _cts?.Dispose(); } base.Dispose(disposing); } protected override Task OnInitializedAsync() { _persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData); return base.OnInitializedAsync(); } private Task PersistData() { ApplicationState.PersistAsJson("_movies", _movies); ApplicationState.PersistAsJson("_shows", _shows); ApplicationState.PersistAsJson("_seasons", _seasons); ApplicationState.PersistAsJson("_episodes", _episodes); ApplicationState.PersistAsJson("_musicVideos", _musicVideos); ApplicationState.PersistAsJson("_otherVideos", _otherVideos); ApplicationState.PersistAsJson("_songs", _songs); ApplicationState.PersistAsJson("_images", _images); ApplicationState.PersistAsJson("_remoteStreams", _remoteStreams); ApplicationState.PersistAsJson("_artists", _artists); return Task.CompletedTask; } protected override async Task OnParametersSetAsync() { _cts?.Cancel(); _cts?.Dispose(); _cts = new CancellationTokenSource(); var token = _cts.Token; await RefreshData(token); await InvokeAsync(StateHasChanged); } protected override async Task RefreshData(CancellationToken token) { try { _query = "state:FileNotFound"; if (string.IsNullOrWhiteSpace(_query)) { return; } var loadingTasks = new List { LoadDataAsync("_movies", () => Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50), token), r => _movies = r), LoadDataAsync("_shows", () => Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50), token), r => _shows = r), LoadDataAsync("_seasons", () => Mediator.Send(new QuerySearchIndexSeasons($"type:season AND ({_query})", 1, 50), token), r => _seasons = r), LoadDataAsync("_episodes", () => Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50), token), r => _episodes = r), LoadDataAsync("_musicVideos", () => Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50), token), r => _musicVideos = r), LoadDataAsync("_otherVideos", () => Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50), token), r => _otherVideos = r), LoadDataAsync("_songs", () => Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50), token), r => _songs = r), LoadDataAsync("_images", () => Mediator.Send(new QuerySearchIndexImages($"type:image AND ({_query})", 1, 50), token), r => _images = r), LoadDataAsync("_remoteStreams", () => Mediator.Send(new QuerySearchIndexRemoteStreams($"type:remote_stream AND ({_query})", 1, 50), token), r => _remoteStreams = r), LoadDataAsync("_artists", () => Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), token), r => _artists = r) }; await Task.WhenAll(loadingTasks); } catch (OperationCanceledException) { // do nothing } } private async Task LoadDataAsync(string key, Func> fetch, Action assign) { if (!ApplicationState.TryTakeFromJson(key, out T restored)) { assign(await fetch()); } else { assign(restored); } } private bool IsNotEmpty => _movies?.Cards.Count > 0 || _shows?.Cards.Count > 0 || _seasons?.Cards.Count > 0 || _episodes?.Cards.Count > 0 || _musicVideos?.Cards.Count > 0 || _otherVideos?.Cards.Count > 0 || _songs?.Cards.Count > 0 || _artists?.Cards.Count > 0 || _images?.Cards.Count > 0 || _remoteStreams?.Cards.Count > 0; private IEnumerable AllCardsOnPage() => Enumerable.Empty() .Concat(_movies.Cards) .Concat(_shows.Cards) .Concat(_seasons.Cards) .Concat(_episodes.Cards) .Concat(_artists.Cards) .Concat(_musicVideos.Cards) .Concat(_otherVideos.Cards) .Concat(_songs.Cards) .Concat(_images.Cards) .Concat(_remoteStreams.Cards); private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { List GetSortedItems() { return _movies.Cards.OrderBy(m => m.SortTitle) .Append(_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)) .Append(_images.Cards.OrderBy(i => i.SortTitle)) .Append(_remoteStreams.Cards.OrderBy(rs => rs.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 string GetImagesLink() { var uri = "media/images/page/1"; if (!string.IsNullOrWhiteSpace(_query)) { (string key, string value) = _query.EncodeQuery(); uri = $"{uri}?{key}={value}"; } return uri; } private string GetRemoteStreamsLink() { var uri = "media/remote/streams/page/1"; if (!string.IsNullOrWhiteSpace(_query)) { (string key, string value) = _query.EncodeQuery(); uri = $"{uri}?{key}={value}"; } return uri; } private Task DeleteFromDatabase() => DeleteItemsFromDatabase( SelectedItems.OfType().Map(m => m.MovieId).ToList(), SelectedItems.OfType().Map(s => s.TelevisionShowId).ToList(), SelectedItems.OfType().Map(s => s.TelevisionSeasonId).ToList(), SelectedItems.OfType().Map(e => e.EpisodeId).ToList(), SelectedItems.OfType().Map(a => a.ArtistId).ToList(), SelectedItems.OfType().Map(mv => mv.MusicVideoId).ToList(), SelectedItems.OfType().Map(ov => ov.OtherVideoId).ToList(), SelectedItems.OfType().Map(s => s.SongId).ToList(), SelectedItems.OfType().Map(i => i.ImageId).ToList(), SelectedItems.OfType().Map(i => i.RemoteStreamId).ToList()); private async Task DeleteItemsFromDatabase( List movieIds, List showIds, List seasonIds, List episodeIds, List artistIds, List musicVideoIds, List otherVideoIds, List songIds, List imageIds, List remoteStreamIds, string entityName = "selected items") { int count = movieIds.Count + showIds.Count + seasonIds.Count + episodeIds.Count + artistIds.Count + musicVideoIds.Count + otherVideoIds.Count + songIds.Count + imageIds.Count; var parameters = new DialogParameters { { "EntityType", count.ToString() }, { "EntityName", entityName } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; IDialogReference dialog = await Dialog.ShowAsync("Delete From Database", parameters, options); DialogResult result = await dialog.Result; if (result is { Canceled: false }) { var request = new DeleteItemsFromDatabase( movieIds.Append(showIds) .Append(seasonIds) .Append(episodeIds) .Append(artistIds) .Append(musicVideoIds) .Append(otherVideoIds) .Append(songIds) .Append(imageIds) .Append(remoteStreamIds) .ToList()); using var cts = new CancellationTokenSource(); Either addResult = await Mediator.Send(request, cts.Token); 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(cts.Token); }); } } private async Task DeleteItemFromDatabase(MediaCardViewModel vm) { DeleteItemsFromDatabase request; switch (vm) { case MovieCardViewModel movie: request = new DeleteItemsFromDatabase([movie.MovieId]); await DeleteItemsWithConfirmation("movie", $"{movie.Title} ({movie.Subtitle})", request); break; case TelevisionShowCardViewModel show: request = new DeleteItemsFromDatabase([show.TelevisionShowId]); await DeleteItemsWithConfirmation("show", $"{show.Title} ({show.Subtitle})", request); break; case TelevisionSeasonCardViewModel season: request = new DeleteItemsFromDatabase([season.TelevisionSeasonId]); await DeleteItemsWithConfirmation("season", $"{season.Title} ({season.Subtitle})", request); break; case TelevisionEpisodeCardViewModel episode: request = new DeleteItemsFromDatabase([episode.EpisodeId]); await DeleteItemsWithConfirmation("episode", $"{episode.Title} ({episode.Subtitle})", request); break; case ArtistCardViewModel artist: request = new DeleteItemsFromDatabase([artist.ArtistId]); await DeleteItemsWithConfirmation("artist", $"{artist.Title} ({artist.Subtitle})", request); break; case MusicVideoCardViewModel musicVideo: request = new DeleteItemsFromDatabase([musicVideo.MusicVideoId]); await DeleteItemsWithConfirmation("music video", $"{musicVideo.Title} ({musicVideo.Subtitle})", request); break; case OtherVideoCardViewModel otherVideo: request = new DeleteItemsFromDatabase([otherVideo.OtherVideoId]); await DeleteItemsWithConfirmation("other video", $"{otherVideo.Title} ({otherVideo.Subtitle})", request); break; case SongCardViewModel song: request = new DeleteItemsFromDatabase([song.SongId]); await DeleteItemsWithConfirmation("song", $"{song.Title} ({song.Subtitle})", request); break; case ImageCardViewModel image: request = new DeleteItemsFromDatabase([image.ImageId]); await DeleteItemsWithConfirmation("image", $"{image.Title} ({image.Subtitle})", request); break; case RemoteStreamCardViewModel remoteStream: request = new DeleteItemsFromDatabase([remoteStream.RemoteStreamId]); await DeleteItemsWithConfirmation("remote stream", $"{remoteStream.Title} ({remoteStream.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 = await Dialog.ShowAsync("Delete From Database", parameters, options); DialogResult result = await dialog.Result; if (result is not null && !result.Canceled) { var token = _cts?.Token ?? CancellationToken.None; await Mediator.Send(request, token); await RefreshData(token); } } private async Task EmptyTrash() { int count = _movies.Count + _shows.Count + _seasons.Count + _episodes.Count + _artists.Count + _musicVideos.Count + _otherVideos.Count + _songs.Count + _images.Count; var parameters = new DialogParameters { { "EntityType", count.ToString() }, { "EntityName", "missing items" } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; IDialogReference dialog = await Dialog.ShowAsync("Delete From Database", parameters, options); DialogResult result = await dialog.Result; if (result is not null && !result.Canceled) { var token = _cts?.Token ?? CancellationToken.None; Either emptyTrashResult = await Mediator.Send(new EmptyTrash(), token); foreach (BaseError error in emptyTrashResult.LeftToSeq()) { Snackbar.Add(error.Value, Severity.Error); Logger.LogError("Unexpected error emptying trash: {Error}", error.Value); } if (emptyTrashResult.IsRight) { await RefreshData(token); await InvokeAsync(StateHasChanged); } } } }