Browse Source

fix emptying trash (#1905)

pull/1906/head
Jason Dove 2 years ago committed by GitHub
parent
commit
7bba422880
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 30
      ErsatzTV.Application/Maintenance/Commands/EmptyTrashHandler.cs
  3. 2
      ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs
  4. 17
      ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs
  5. 4
      ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs
  6. 162
      ErsatzTV/Pages/Trash.razor

2
CHANGELOG.md

@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Fixed
- Fix startup error with MySql backend caused by database cleaner
- Fix emptying trash with ElasticSearch backend
- Fix double loading of trash UI elements, and fix reloading of all UI elements after emptying trash
## [0.8.8-beta] - 2024-09-19
### Added

30
ErsatzTV.Application/Maintenance/Commands/EmptyTrashHandler.cs

@ -3,7 +3,6 @@ using ErsatzTV.Core; @@ -3,7 +3,6 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Search;
using ErsatzTV.Infrastructure.Search;
namespace ErsatzTV.Application.Maintenance;
@ -27,33 +26,16 @@ public class EmptyTrashHandler : IRequestHandler<EmptyTrash, Either<BaseError, U @@ -27,33 +26,16 @@ public class EmptyTrashHandler : IRequestHandler<EmptyTrash, Either<BaseError, U
EmptyTrash request,
CancellationToken cancellationToken)
{
string[] types =
{
LuceneSearchIndex.MovieType,
LuceneSearchIndex.ShowType,
LuceneSearchIndex.SeasonType,
LuceneSearchIndex.EpisodeType,
LuceneSearchIndex.MusicVideoType,
LuceneSearchIndex.OtherVideoType,
LuceneSearchIndex.SongType,
LuceneSearchIndex.ArtistType
};
var ids = new List<int>();
foreach (string type in types)
{
SearchResult result = await _searchIndex.Search(_client, $"type:{type} AND (state:FileNotFound)", 0, 0);
ids.AddRange(result.Items.Map(i => i.Id));
}
SearchResult result = await _searchIndex.Search(_client, "state:FileNotFound", 0, 10_000);
var ids = result.Items.Map(i => i.Id).ToList();
Either<BaseError, Unit> deleteResult = await _mediaItemRepository.DeleteItems(ids);
if (deleteResult.IsRight)
// ElasticSearch remove items may fail, so do that first
if (await _searchIndex.RemoveItems(ids))
{
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
return await _mediaItemRepository.DeleteItems(ids);
}
return deleteResult;
return BaseError.New("Failed to empty trash");
}
}

2
ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs

@ -24,7 +24,7 @@ public interface ISearchIndex : IDisposable @@ -24,7 +24,7 @@ public interface ISearchIndex : IDisposable
IFallbackMetadataProvider fallbackMetadataProvider,
List<MediaItem> items);
Task<Unit> RemoveItems(IEnumerable<int> ids);
Task<bool> RemoveItems(IEnumerable<int> ids);
Task<SearchResult> Search(IClient client, string query, int skip, int limit);
void Commit();
}

17
ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs

@ -2,6 +2,7 @@ using System.Globalization; @@ -2,6 +2,7 @@ using System.Globalization;
using Bugsnag;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Aggregations;
using Elastic.Clients.Elasticsearch.Core.Bulk;
using Elastic.Clients.Elasticsearch.IndexManagement;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
@ -146,15 +147,17 @@ public class ElasticSearchIndex : ISearchIndex @@ -146,15 +147,17 @@ public class ElasticSearchIndex : ISearchIndex
return Unit.Default;
}
public async Task<Unit> RemoveItems(IEnumerable<int> ids)
public async Task<bool> RemoveItems(IEnumerable<int> ids)
{
await _client.BulkAsync(
descriptor => descriptor
.Index(IndexName)
.DeleteMany(ids.Map(id => new Id(id)))
);
var deleteBulkRequest = new BulkRequest { Operations = [] };
foreach (int id in ids)
{
var deleteOperation = new BulkDeleteOperation<ElasticSearchItem>(new Id(id)) { Index = IndexName };
deleteBulkRequest.Operations.Add(deleteOperation);
}
return Unit.Default;
BulkResponse deleteResponse = await _client.BulkAsync(deleteBulkRequest).ConfigureAwait(false);
return deleteResponse.IsValidResponse;
}
public async Task<SearchResult> Search(IClient client, string query, int skip, int limit)

4
ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs

@ -186,14 +186,14 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -186,14 +186,14 @@ public sealed class LuceneSearchIndex : ISearchIndex
return Unit.Default;
}
public Task<Unit> RemoveItems(IEnumerable<int> ids)
public Task<bool> RemoveItems(IEnumerable<int> ids)
{
foreach (int id in ids)
{
_writer.DeleteDocuments(new Term(IdField, id.ToString(CultureInfo.InvariantCulture)));
}
return Task.FromResult(Unit.Default);
return Task.FromResult(true);
}
public Task<SearchResult> Search(IClient client, string query, int skip, int limit)

162
ErsatzTV/Pages/Trash.razor

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
@using ErsatzTV.Extensions
@inherits MultiSelectBase<Search>
@inject NavigationManager NavigationManager
@inject PersistentComponentState ApplicationState
<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">
@ -29,47 +30,47 @@ @@ -29,47 +30,47 @@
}
else
{
if (_movies?.Count > 0)
if (_movies?.Cards.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)
if (_shows?.Cards.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)
if (_seasons?.Cards.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)
if (_episodes?.Cards.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)
if (_artists?.Cards.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)
if (_musicVideos?.Cards.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)
if (_otherVideos?.Cards.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)
if (_songs?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#songs")" Style="margin-bottom: auto; margin-top: auto">@_songs.Count Songs</MudLink>
}
if (_images?.Count > 0)
if (_images?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")" Style="margin-bottom: auto; margin-top: auto">@_images.Count Images</MudLink>
}
@ -93,7 +94,7 @@ @@ -93,7 +94,7 @@
</div>
</MudPaper>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Style="margin-top: 96px">
@if (_movies?.Count > 0)
@if (_movies?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
@ -121,7 +122,7 @@ @@ -121,7 +122,7 @@
</MudContainer>
}
@if (_shows?.Count > 0)
@if (_shows?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
@ -149,7 +150,7 @@ @@ -149,7 +150,7 @@
</MudContainer>
}
@if (_seasons?.Count > 0)
@if (_seasons?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
@ -177,7 +178,7 @@ @@ -177,7 +178,7 @@
</MudContainer>
}
@if (_episodes?.Count > 0)
@if (_episodes?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
@ -206,7 +207,7 @@ @@ -206,7 +207,7 @@
</MudContainer>
}
@if (_artists?.Count > 0)
@if (_artists?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
@ -235,7 +236,7 @@ @@ -235,7 +236,7 @@
</MudContainer>
}
@if (_musicVideos?.Count > 0)
@if (_musicVideos?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
@ -264,7 +265,7 @@ @@ -264,7 +265,7 @@
</MudContainer>
}
@if (_otherVideos?.Count > 0)
@if (_otherVideos?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
@ -293,7 +294,7 @@ @@ -293,7 +294,7 @@
</MudContainer>
}
@if (_songs?.Count > 0)
@if (_songs?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
@ -322,7 +323,7 @@ @@ -322,7 +323,7 @@
</MudContainer>
}
@if (_images?.Count > 0)
@if (_images?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
@ -353,6 +354,8 @@ @@ -353,6 +354,8 @@
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private string _query;
private MovieCardResultsViewModel _movies;
private TelevisionShowCardResultsViewModel _shows;
@ -364,27 +367,102 @@ @@ -364,27 +367,102 @@
private ImageCardResultsViewModel _images;
private ArtistCardResultsViewModel _artists;
protected override Task OnInitializedAsync() => RefreshData();
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("_artists", _artists);
return Task.CompletedTask;
}
protected override async Task OnParametersSetAsync()
{
await RefreshData();
await InvokeAsync(StateHasChanged);
}
protected override async Task RefreshData()
{
_query = "state:FileNotFound";
if (!string.IsNullOrWhiteSpace(_query))
{
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50), CancellationToken);
_shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50), CancellationToken);
_seasons = await Mediator.Send(new QuerySearchIndexSeasons($"type:season AND ({_query})", 1, 50), CancellationToken);
_episodes = await Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50), CancellationToken);
_musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50), CancellationToken);
_otherVideos = await Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50), CancellationToken);
_songs = await Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50), CancellationToken);
_images = await Mediator.Send(new QuerySearchIndexImages($"type:image AND ({_query})", 1, 50), CancellationToken);
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), CancellationToken);
if (!ApplicationState.TryTakeFromJson("_movies", out _movies))
{
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50), _cts.Token);
}
if (!ApplicationState.TryTakeFromJson("_shows", out _shows))
{
_shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50), _cts.Token);
}
if (!ApplicationState.TryTakeFromJson("_seasons", out _seasons))
{
_seasons = await Mediator.Send(new QuerySearchIndexSeasons($"type:season AND ({_query})", 1, 50), _cts.Token);
}
if (!ApplicationState.TryTakeFromJson("_episodes", out _episodes))
{
_episodes = await Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50), _cts.Token);
}
if (!ApplicationState.TryTakeFromJson("_musicVideos", out _musicVideos))
{
_musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50), _cts.Token);
}
if (!ApplicationState.TryTakeFromJson("_otherVideos", out _otherVideos))
{
_otherVideos = await Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50), _cts.Token);
}
if (!ApplicationState.TryTakeFromJson("_songs", out _songs))
{
_songs = await Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50), _cts.Token);
}
if (!ApplicationState.TryTakeFromJson("_images", out _images))
{
_images = await Mediator.Send(new QuerySearchIndexImages($"type:image AND ({_query})", 1, 50), _cts.Token);
}
if (!ApplicationState.TryTakeFromJson("_artists", out _artists))
{
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), _cts.Token);
}
}
}
private bool IsNotEmpty =>
_movies?.Count > 0 || _shows?.Count > 0 || _seasons?.Count > 0 || _episodes?.Count > 0 || _musicVideos?.Count > 0 || _otherVideos?.Count > 0 || _songs?.Count > 0 || _artists?.Count > 0 || _images?.Count > 0;
_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;
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
{
@ -558,7 +636,7 @@ @@ -558,7 +636,7 @@
.Append(imageIds)
.ToList());
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token);
await addResult.Match(
Left: error =>
{
@ -628,11 +706,11 @@ @@ -628,11 +706,11 @@
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);
IDialogReference dialog = await Dialog.ShowAsync<DeleteFromDatabaseDialog>("Delete From Database", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
if (result is not null && !result.Canceled)
{
await Mediator.Send(request, CancellationToken);
await Mediator.Send(request, _cts.Token);
await RefreshData();
}
}
@ -645,12 +723,22 @@ @@ -645,12 +723,22 @@
var parameters = new DialogParameters { { "EntityType", count.ToString() }, { "EntityName", "missing items" } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = Dialog.Show<DeleteFromDatabaseDialog>("Delete From Database", parameters, options);
IDialogReference dialog = await Dialog.ShowAsync<DeleteFromDatabaseDialog>("Delete From Database", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
if (result is not null && !result.Canceled)
{
await Mediator.Send(new EmptyTrash(), CancellationToken);
await RefreshData();
Either<BaseError, Unit> emptyTrashResult = await Mediator.Send(new EmptyTrash(), _cts.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();
await InvokeAsync(StateHasChanged);
}
}
}

Loading…
Cancel
Save