Browse Source

use cancellation tokens in ui (#666)

* use cancellation tokens in pages

* use cancellation token in shared ui

* use cancellation tokens in artwork controller
pull/667/head
Jason Dove 4 years ago committed by GitHub
parent
commit
452f361384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      ErsatzTV.Core/Interfaces/GitHub/IGitHubApiClient.cs
  2. 9
      ErsatzTV.Infrastructure/GitHub/GitHubApiClient.cs
  3. 5
      ErsatzTV.Infrastructure/GitHub/IGitHubApi.cs
  4. 68
      ErsatzTV/Controllers/ArtworkController.cs
  5. 19
      ErsatzTV/Pages/Artist.razor
  6. 4
      ErsatzTV/Pages/ArtistList.razor
  7. 41
      ErsatzTV/Pages/ChannelEditor.razor
  8. 20
      ErsatzTV/Pages/Channels.razor
  9. 17
      ErsatzTV/Pages/CollectionEditor.razor
  10. 6
      ErsatzTV/Pages/CollectionItems.razor
  11. 34
      ErsatzTV/Pages/Collections.razor
  12. 11
      ErsatzTV/Pages/EmbyLibrariesEditor.razor
  13. 13
      ErsatzTV/Pages/EmbyMediaSourceEditor.razor
  14. 11
      ErsatzTV/Pages/EmbyMediaSources.razor
  15. 13
      ErsatzTV/Pages/EmbyPathReplacementsEditor.razor
  16. 4
      ErsatzTV/Pages/EpisodeList.razor
  17. 20
      ErsatzTV/Pages/FFmpeg.razor
  18. 19
      ErsatzTV/Pages/FFmpegEditor.razor
  19. 26
      ErsatzTV/Pages/FillerPresetEditor.razor
  20. 19
      ErsatzTV/Pages/FillerPresets.razor
  21. 13
      ErsatzTV/Pages/FragmentNavigationBase.cs
  22. 15
      ErsatzTV/Pages/Index.razor
  23. 19
      ErsatzTV/Pages/JellyfinLibrariesEditor.razor
  24. 13
      ErsatzTV/Pages/JellyfinMediaSourceEditor.razor
  25. 11
      ErsatzTV/Pages/JellyfinMediaSources.razor
  26. 17
      ErsatzTV/Pages/JellyfinPathReplacementsEditor.razor
  27. 19
      ErsatzTV/Pages/Libraries.razor
  28. 18
      ErsatzTV/Pages/LocalLibraries.razor
  29. 17
      ErsatzTV/Pages/LocalLibraryEditor.razor
  30. 15
      ErsatzTV/Pages/LocalLibraryPathEditor.razor
  31. 21
      ErsatzTV/Pages/Logs.razor
  32. 15
      ErsatzTV/Pages/Movie.razor
  33. 11
      ErsatzTV/Pages/MovieList.razor
  34. 20
      ErsatzTV/Pages/MultiCollectionEditor.razor
  35. 6
      ErsatzTV/Pages/MultiSelectBase.cs
  36. 4
      ErsatzTV/Pages/MusicVideoList.razor
  37. 4
      ErsatzTV/Pages/OtherVideoList.razor
  38. 15
      ErsatzTV/Pages/PlayoutEditor.razor
  39. 30
      ErsatzTV/Pages/Playouts.razor
  40. 19
      ErsatzTV/Pages/PlexLibrariesEditor.razor
  41. 1
      ErsatzTV/Pages/PlexMediaSources.razor
  42. 17
      ErsatzTV/Pages/PlexPathReplacementsEditor.razor
  43. 17
      ErsatzTV/Pages/ScheduleEditor.razor
  44. 29
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  45. 24
      ErsatzTV/Pages/Schedules.razor
  46. 32
      ErsatzTV/Pages/Search.razor
  47. 34
      ErsatzTV/Pages/Settings.razor
  48. 4
      ErsatzTV/Pages/SongList.razor
  49. 19
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  50. 19
      ErsatzTV/Pages/TelevisionSeasonList.razor
  51. 4
      ErsatzTV/Pages/TelevisionSeasonSearchResults.razor
  52. 4
      ErsatzTV/Pages/TelevisionShowList.razor
  53. 22
      ErsatzTV/Pages/TraktLists.razor
  54. 20
      ErsatzTV/Pages/Trash.razor
  55. 22
      ErsatzTV/Pages/WatermarkEditor.razor
  56. 14
      ErsatzTV/Pages/Watermarks.razor
  57. 13
      ErsatzTV/Shared/AddToCollectionDialog.razor
  58. 11
      ErsatzTV/Shared/AddToScheduleDialog.razor
  59. 11
      ErsatzTV/Shared/CopyFFmpegProfileDialog.razor
  60. 11
      ErsatzTV/Shared/CopyWatermarkDialog.razor
  61. 13
      ErsatzTV/Shared/MoveLocalLibraryPathDialog.razor
  62. 11
      ErsatzTV/Shared/RemoteMediaSourceLibrariesEditor.razor
  63. 11
      ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor
  64. 13
      ErsatzTV/Shared/RemoteMediaSources.razor
  65. 13
      ErsatzTV/Shared/SaveAsSmartCollectionDialog.razor
  66. 11
      ErsatzTV/Shared/SchedulePlayoutRebuild.razor

7
ErsatzTV.Core/Interfaces/GitHub/IGitHubApiClient.cs

@ -1,11 +1,12 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using LanguageExt; using LanguageExt;
namespace ErsatzTV.Core.Interfaces.GitHub namespace ErsatzTV.Core.Interfaces.GitHub
{ {
public interface IGitHubApiClient public interface IGitHubApiClient
{ {
Task<Either<BaseError, string>> GetLatestReleaseNotes(); Task<Either<BaseError, string>> GetLatestReleaseNotes(CancellationToken cancellationToken);
Task<Either<BaseError, string>> GetReleaseNotes(string tag); Task<Either<BaseError, string>> GetReleaseNotes(string tag, CancellationToken cancellationToken);
} }
} }

9
ErsatzTV.Infrastructure/GitHub/GitHubApiClient.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.GitHub; using ErsatzTV.Core.Interfaces.GitHub;
@ -9,12 +10,12 @@ namespace ErsatzTV.Infrastructure.GitHub
{ {
public class GitHubApiClient : IGitHubApiClient public class GitHubApiClient : IGitHubApiClient
{ {
public async Task<Either<BaseError, string>> GetLatestReleaseNotes() public async Task<Either<BaseError, string>> GetLatestReleaseNotes(CancellationToken cancellationToken)
{ {
try try
{ {
IGitHubApi service = RestService.For<IGitHubApi>("https://api.github.com"); IGitHubApi service = RestService.For<IGitHubApi>("https://api.github.com");
return await service.GetReleases().Map(releases => releases.Head().Body); return await service.GetReleases(cancellationToken).Map(releases => releases.Head().Body);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -22,12 +23,12 @@ namespace ErsatzTV.Infrastructure.GitHub
} }
} }
public async Task<Either<BaseError, string>> GetReleaseNotes(string tag) public async Task<Either<BaseError, string>> GetReleaseNotes(string tag, CancellationToken cancellationToken)
{ {
try try
{ {
IGitHubApi service = RestService.For<IGitHubApi>("https://api.github.com"); IGitHubApi service = RestService.For<IGitHubApi>("https://api.github.com");
return await service.GetTag(tag).Map(t => t.Body); return await service.GetTag(tag, cancellationToken).Map(t => t.Body);
} }
catch (Exception ex) catch (Exception ex)
{ {

5
ErsatzTV.Infrastructure/GitHub/IGitHubApi.cs

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Infrastructure.GitHub.Models; using ErsatzTV.Infrastructure.GitHub.Models;
using Refit; using Refit;
@ -9,9 +10,9 @@ namespace ErsatzTV.Infrastructure.GitHub
public interface IGitHubApi public interface IGitHubApi
{ {
[Get("/repos/jasongdove/ErsatzTV/releases")] [Get("/repos/jasongdove/ErsatzTV/releases")]
public Task<List<GitHubTag>> GetReleases(); public Task<List<GitHubTag>> GetReleases(CancellationToken cancellationToken);
[Get("/repos/jasongdove/ErsatzTV/releases/tags/{tag}")] [Get("/repos/jasongdove/ErsatzTV/releases/tags/{tag}")]
public Task<GitHubTag> GetTag(string tag); public Task<GitHubTag> GetTag(string tag, CancellationToken cancellationToken);
} }
} }

68
ErsatzTV/Controllers/ArtworkController.cs

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Emby; using ErsatzTV.Application.Emby;
using ErsatzTV.Application.Emby.Queries; using ErsatzTV.Application.Emby.Queries;
@ -40,30 +41,30 @@ namespace ErsatzTV.Controllers
[HttpHead("/iptv/artwork/posters/{fileName}.jpg")] [HttpHead("/iptv/artwork/posters/{fileName}.jpg")]
[HttpGet("/iptv/artwork/posters/{fileName}.jpg")] [HttpGet("/iptv/artwork/posters/{fileName}.jpg")]
[HttpGet("/artwork/posters/{fileName}")] [HttpGet("/artwork/posters/{fileName}")]
public async Task<IActionResult> GetPoster(string fileName) public async Task<IActionResult> GetPoster(string fileName, CancellationToken cancellationToken)
{ {
Either<BaseError, CachedImagePathViewModel> cachedImagePath = Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Poster, 440)); await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Poster, 440), cancellationToken);
return cachedImagePath.Match<IActionResult>( return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(), Left: _ => new NotFoundResult(),
Right: r => new PhysicalFileResult(r.FileName, r.MimeType)); Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
} }
[HttpGet("/artwork/watermarks/{fileName}")] [HttpGet("/artwork/watermarks/{fileName}")]
public async Task<IActionResult> GetWatermark(string fileName) public async Task<IActionResult> GetWatermark(string fileName, CancellationToken cancellationToken)
{ {
Either<BaseError, CachedImagePathViewModel> cachedImagePath = Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Watermark)); await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Watermark), cancellationToken);
return cachedImagePath.Match<IActionResult>( return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(), Left: _ => new NotFoundResult(),
Right: r => new PhysicalFileResult(r.FileName, r.MimeType)); Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
} }
[HttpGet("/artwork/fanart/{fileName}")] [HttpGet("/artwork/fanart/{fileName}")]
public async Task<IActionResult> GetFanArt(string fileName) public async Task<IActionResult> GetFanArt(string fileName, CancellationToken cancellationToken)
{ {
Either<BaseError, CachedImagePathViewModel> cachedImagePath = Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.FanArt)); await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.FanArt), cancellationToken);
return cachedImagePath.Match<IActionResult>( return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(), Left: _ => new NotFoundResult(),
Right: r => new PhysicalFileResult(r.FileName, r.MimeType)); Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
@ -77,14 +78,14 @@ namespace ErsatzTV.Controllers
[HttpGet("/iptv/artwork/thumbnails/jellyfin/{*path}")] [HttpGet("/iptv/artwork/thumbnails/jellyfin/{*path}")]
[HttpGet("/artwork/thumbnails/jellyfin/{*path}")] [HttpGet("/artwork/thumbnails/jellyfin/{*path}")]
[HttpGet("/artwork/fanart/jellyfin/{*path}")] [HttpGet("/artwork/fanart/jellyfin/{*path}")]
public Task<IActionResult> GetJellyfin(string path) public Task<IActionResult> GetJellyfin(string path, CancellationToken cancellationToken)
{ {
if (Request.QueryString.HasValue) if (Request.QueryString.HasValue)
{ {
path += Request.QueryString.Value; path += Request.QueryString.Value;
} }
return GetJellyfinArtwork(path); return GetJellyfinArtwork(path, cancellationToken);
} }
[HttpHead("/iptv/artwork/posters/emby/{*path}")] [HttpHead("/iptv/artwork/posters/emby/{*path}")]
@ -94,54 +95,54 @@ namespace ErsatzTV.Controllers
[HttpGet("/iptv/artwork/thumbnails/emby/{*path}")] [HttpGet("/iptv/artwork/thumbnails/emby/{*path}")]
[HttpGet("/artwork/thumbnails/emby/{*path}")] [HttpGet("/artwork/thumbnails/emby/{*path}")]
[HttpGet("/artwork/fanart/emby/{*path}")] [HttpGet("/artwork/fanart/emby/{*path}")]
public Task<IActionResult> GetEmby(string path) public Task<IActionResult> GetEmby(string path, CancellationToken cancellationToken)
{ {
if (Request.QueryString.HasValue) if (Request.QueryString.HasValue)
{ {
path += Request.QueryString.Value; path += Request.QueryString.Value;
} }
return GetEmbyArtwork(path); return GetEmbyArtwork(path, cancellationToken);
} }
[HttpHead("/iptv/artwork/posters/plex/{plexMediaSourceId}/{*path}")] [HttpHead("/iptv/artwork/posters/plex/{plexMediaSourceId}/{*path}")]
[HttpGet("/iptv/artwork/posters/plex/{plexMediaSourceId}/{*path}")] [HttpGet("/iptv/artwork/posters/plex/{plexMediaSourceId}/{*path}")]
[HttpGet("/artwork/posters/plex/{plexMediaSourceId}/{*path}")] [HttpGet("/artwork/posters/plex/{plexMediaSourceId}/{*path}")]
public Task<IActionResult> GetPlexPoster(int plexMediaSourceId, string path) => public Task<IActionResult> GetPlexPoster(int plexMediaSourceId, string path, CancellationToken cancellationToken) =>
GetPlexArtwork( GetPlexArtwork(
plexMediaSourceId, plexMediaSourceId,
$"photo/:/transcode?url=/{path}&height=440&width=304&minSize=1&upscale=0"); $"photo/:/transcode?url=/{path}&height=440&width=304&minSize=1&upscale=0",
cancellationToken);
[HttpGet("/artwork/fanart/plex/{plexMediaSourceId}/{*path}")] [HttpGet("/artwork/fanart/plex/{plexMediaSourceId}/{*path}")]
public Task<IActionResult> GetPlexFanArt(int plexMediaSourceId, string path) => public Task<IActionResult> GetPlexFanArt(int plexMediaSourceId, string path, CancellationToken cancellationToken) =>
GetPlexArtwork( GetPlexArtwork(plexMediaSourceId, $"/{path}", cancellationToken);
plexMediaSourceId,
$"/{path}");
[HttpGet("/artwork/thumbnails/plex/{plexMediaSourceId}/{*path}")] [HttpGet("/artwork/thumbnails/plex/{plexMediaSourceId}/{*path}")]
public Task<IActionResult> GetPlexThumbnail(int plexMediaSourceId, string path) => public Task<IActionResult> GetPlexThumbnail(int plexMediaSourceId, string path, CancellationToken cancellationToken) =>
GetPlexArtwork( GetPlexArtwork(
plexMediaSourceId, plexMediaSourceId,
$"photo/:/transcode?url=/{path}&height=220&width=392&minSize=1&upscale=0"); $"photo/:/transcode?url=/{path}&height=220&width=392&minSize=1&upscale=0",
cancellationToken);
[HttpHead("/iptv/artwork/thumbnails/{fileName}")] [HttpHead("/iptv/artwork/thumbnails/{fileName}")]
[HttpGet("/iptv/artwork/thumbnails/{fileName}")] [HttpGet("/iptv/artwork/thumbnails/{fileName}")]
[HttpHead("/iptv/artwork/thumbnails/{fileName}.jpg")] [HttpHead("/iptv/artwork/thumbnails/{fileName}.jpg")]
[HttpGet("/iptv/artwork/thumbnails/{fileName}.jpg")] [HttpGet("/iptv/artwork/thumbnails/{fileName}.jpg")]
[HttpGet("/artwork/thumbnails/{fileName}")] [HttpGet("/artwork/thumbnails/{fileName}")]
public async Task<IActionResult> GetThumbnail(string fileName) public async Task<IActionResult> GetThumbnail(string fileName, CancellationToken cancellationToken)
{ {
Either<BaseError, CachedImagePathViewModel> cachedImagePath = Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Thumbnail, 220)); await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Thumbnail, 220), cancellationToken);
return cachedImagePath.Match<IActionResult>( return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(), Left: _ => new NotFoundResult(),
Right: r => new PhysicalFileResult(r.FileName, r.MimeType)); Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
} }
private async Task<IActionResult> GetPlexArtwork(int plexMediaSourceId, string transcodePath) private async Task<IActionResult> GetPlexArtwork(int plexMediaSourceId, string transcodePath, CancellationToken cancellationToken)
{ {
Either<BaseError, PlexConnectionParametersViewModel> connectionParameters = Either<BaseError, PlexConnectionParametersViewModel> connectionParameters =
await _mediator.Send(new GetPlexConnectionParameters(plexMediaSourceId)); await _mediator.Send(new GetPlexConnectionParameters(plexMediaSourceId), cancellationToken);
return await connectionParameters.Match<Task<IActionResult>>( return await connectionParameters.Match<Task<IActionResult>>(
Left: _ => new NotFoundResult().AsTask<IActionResult>(), Left: _ => new NotFoundResult().AsTask<IActionResult>(),
@ -153,8 +154,9 @@ namespace ErsatzTV.Controllers
var fullPath = new Uri(r.Uri, transcodePath); var fullPath = new Uri(r.Uri, transcodePath);
HttpResponseMessage response = await client.GetAsync( HttpResponseMessage response = await client.GetAsync(
fullPath, fullPath,
HttpCompletionOption.ResponseHeadersRead); HttpCompletionOption.ResponseHeadersRead,
Stream stream = await response.Content.ReadAsStreamAsync(); cancellationToken);
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken);
return new FileStreamResult( return new FileStreamResult(
stream, stream,
@ -162,10 +164,10 @@ namespace ErsatzTV.Controllers
}); });
} }
private async Task<IActionResult> GetJellyfinArtwork(string path) private async Task<IActionResult> GetJellyfinArtwork(string path, CancellationToken cancellationToken)
{ {
Either<BaseError, JellyfinConnectionParametersViewModel> connectionParameters = Either<BaseError, JellyfinConnectionParametersViewModel> connectionParameters =
await _mediator.Send(new GetJellyfinConnectionParameters()); await _mediator.Send(new GetJellyfinConnectionParameters(), cancellationToken);
return await connectionParameters.Match<Task<IActionResult>>( return await connectionParameters.Match<Task<IActionResult>>(
Left: _ => new NotFoundResult().AsTask<IActionResult>(), Left: _ => new NotFoundResult().AsTask<IActionResult>(),
@ -176,8 +178,9 @@ namespace ErsatzTV.Controllers
Url fullPath = JellyfinUrl.ForArtwork(vm.Address, path); Url fullPath = JellyfinUrl.ForArtwork(vm.Address, path);
HttpResponseMessage response = await client.GetAsync( HttpResponseMessage response = await client.GetAsync(
fullPath, fullPath,
HttpCompletionOption.ResponseHeadersRead); HttpCompletionOption.ResponseHeadersRead,
Stream stream = await response.Content.ReadAsStreamAsync(); cancellationToken);
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken);
return new FileStreamResult( return new FileStreamResult(
stream, stream,
@ -185,10 +188,10 @@ namespace ErsatzTV.Controllers
}); });
} }
private async Task<IActionResult> GetEmbyArtwork(string path) private async Task<IActionResult> GetEmbyArtwork(string path, CancellationToken cancellationToken)
{ {
Either<BaseError, EmbyConnectionParametersViewModel> connectionParameters = Either<BaseError, EmbyConnectionParametersViewModel> connectionParameters =
await _mediator.Send(new GetEmbyConnectionParameters()); await _mediator.Send(new GetEmbyConnectionParameters(), cancellationToken);
return await connectionParameters.Match<Task<IActionResult>>( return await connectionParameters.Match<Task<IActionResult>>(
Left: _ => new NotFoundResult().AsTask<IActionResult>(), Left: _ => new NotFoundResult().AsTask<IActionResult>(),
@ -199,8 +202,9 @@ namespace ErsatzTV.Controllers
Url fullPath = EmbyUrl.ForArtwork(vm.Address, path); Url fullPath = EmbyUrl.ForArtwork(vm.Address, path);
HttpResponseMessage response = await client.GetAsync( HttpResponseMessage response = await client.GetAsync(
fullPath, fullPath,
HttpCompletionOption.ResponseHeadersRead); HttpCompletionOption.ResponseHeadersRead,
Stream stream = await response.Content.ReadAsStreamAsync(); cancellationToken);
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken);
return new FileStreamResult( return new FileStreamResult(
stream, stream,

19
ErsatzTV/Pages/Artist.razor

@ -8,8 +8,10 @@
@using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.ProgramSchedules.Commands @using ErsatzTV.Application.ProgramSchedules.Commands
@using System.Globalization @using System.Globalization
@using System.Threading
@using ErsatzTV.Extensions @using ErsatzTV.Extensions
@using Unit = LanguageExt.Unit @using Unit = LanguageExt.Unit
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject IDialogService _dialog @inject IDialogService _dialog
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@ -185,6 +187,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int ArtistId { get; set; } public int ArtistId { get; set; }
@ -195,12 +198,18 @@
private List<string> _sortedStyles = new(); private List<string> _sortedStyles = new();
private List<string> _sortedMoods = new(); private List<string> _sortedMoods = new();
private MusicVideoCardResultsViewModel _musicVideos; private MusicVideoCardResultsViewModel _musicVideos;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override Task OnParametersSetAsync() => RefreshData(); protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData() private async Task RefreshData()
{ {
await _mediator.Send(new GetArtistById(ArtistId)).IfSomeAsync(vm => await _mediator.Send(new GetArtistById(ArtistId), _cts.Token).IfSomeAsync(vm =>
{ {
_artist = vm; _artist = vm;
_sortedLanguages = _artist.Languages.OrderBy(ci => ci.EnglishName).ToList(); _sortedLanguages = _artist.Languages.OrderBy(ci => ci.EnglishName).ToList();
@ -209,7 +218,7 @@
_sortedMoods = _artist.Moods.OrderBy(m => m).ToList(); _sortedMoods = _artist.Moods.OrderBy(m => m).ToList();
}); });
_musicVideos = await _mediator.Send(new GetMusicVideoCards(ArtistId, 1, 100)); _musicVideos = await _mediator.Send(new GetMusicVideoCards(ArtistId, 1, 100), _cts.Token);
} }
private async Task AddToCollection() private async Task AddToCollection()
@ -221,7 +230,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
await _mediator.Send(new AddArtistToCollection(collection.Id, ArtistId)); await _mediator.Send(new AddArtistToCollection(collection.Id, ArtistId), _cts.Token);
_navigationManager.NavigateTo($"/media/collections/{collection.Id}"); _navigationManager.NavigateTo($"/media/collections/{collection.Id}");
} }
} }
@ -235,7 +244,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule) if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
{ {
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null)); await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null), _cts.Token);
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); _navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
} }
} }
@ -250,7 +259,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId); var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId);
Either<BaseError, Unit> addResult = await _mediator.Send(request); Either<BaseError, Unit> addResult = await _mediator.Send(request, _cts.Token);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

4
ErsatzTV/Pages/ArtistList.razor

@ -97,7 +97,7 @@
protected override async Task RefreshData() protected override async Task RefreshData()
{ {
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:artist" : $"type:artist AND ({_query})"; string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:artist" : $"type:artist AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexArtists(searchQuery, PageNumber, PageSize)); _data = await Mediator.Send(new QuerySearchIndexArtists(searchQuery, PageNumber, PageSize), CancellationToken);
} }
private void PrevPage() private void PrevPage()
@ -144,7 +144,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddArtistToCollection(collection.Id, artist.ArtistId); var request = new AddArtistToCollection(collection.Id, artist.ArtistId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

41
ErsatzTV/Pages/ChannelEditor.razor

@ -6,6 +6,7 @@
@using ErsatzTV.Application.Images.Commands @using ErsatzTV.Application.Images.Commands
@using ErsatzTV.Application.MediaItems.Queries @using ErsatzTV.Application.MediaItems.Queries
@using System.Globalization @using System.Globalization
@using System.Threading
@using ErsatzTV.Application.Channels @using ErsatzTV.Application.Channels
@using ErsatzTV.Application.Channels.Queries @using ErsatzTV.Application.Channels.Queries
@using ErsatzTV.Application.Filler @using ErsatzTV.Application.Filler
@ -13,6 +14,7 @@
@using ErsatzTV.Application.Watermarks @using ErsatzTV.Application.Watermarks
@using ErsatzTV.Application.Watermarks.Queries @using ErsatzTV.Application.Watermarks.Queries
@using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Core.Domain.Filler
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<ChannelEditor> _logger @inject ILogger<ChannelEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -107,6 +109,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int? Id { get; set; } public int? Id { get; set; }
@ -119,17 +122,23 @@
private List<CultureInfo> _availableCultures; private List<CultureInfo> _availableCultures;
private List<WatermarkViewModel> _watermarks; private List<WatermarkViewModel> _watermarks;
private List<FillerPresetViewModel> _fillerPresets; private List<FillerPresetViewModel> _fillerPresets;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
await LoadFFmpegProfiles(); await LoadFFmpegProfiles(_cts.Token);
_availableCultures = await _mediator.Send(new GetAllLanguageCodes()); _availableCultures = await _mediator.Send(new GetAllLanguageCodes(), _cts.Token);
await LoadWatermarks(); await LoadWatermarks(_cts.Token);
await LoadFillerPresets(); await LoadFillerPresets(_cts.Token);
if (Id.HasValue) if (Id.HasValue)
{ {
Option<ChannelViewModel> maybeChannel = await _mediator.Send(new GetChannelById(Id.Value)); Option<ChannelViewModel> maybeChannel = await _mediator.Send(new GetChannelById(Id.Value), _cts.Token);
maybeChannel.Match( maybeChannel.Match(
channelViewModel => channelViewModel =>
{ {
@ -149,10 +158,10 @@
} }
else else
{ {
FFmpegSettingsViewModel ffmpegSettings = await _mediator.Send(new GetFFmpegSettings()); FFmpegSettingsViewModel ffmpegSettings = await _mediator.Send(new GetFFmpegSettings(), _cts.Token);
// TODO: command for new channel // TODO: command for new channel
IEnumerable<int> channelNumbers = await _mediator.Send(new GetAllChannels()) IEnumerable<int> channelNumbers = await _mediator.Send(new GetAllChannels(), _cts.Token)
.Map(list => list.Map(c => int.TryParse(c.Number.Split(".").Head(), out int result) ? result : 0)); .Map(list => list.Map(c => int.TryParse(c.Number.Split(".").Head(), out int result) ? result : 0));
int maxNumber = Optional(channelNumbers).Flatten().DefaultIfEmpty(0).Max(); int maxNumber = Optional(channelNumbers).Flatten().DefaultIfEmpty(0).Max();
_model.Number = (maxNumber + 1).ToString(); _model.Number = (maxNumber + 1).ToString();
@ -171,14 +180,14 @@
private bool IsEdit => Id.HasValue; private bool IsEdit => Id.HasValue;
private async Task LoadFFmpegProfiles() => private async Task LoadFFmpegProfiles(CancellationToken cancellationToken) =>
_ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles()); _ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles(), cancellationToken);
private async Task LoadWatermarks() => private async Task LoadWatermarks(CancellationToken cancellationToken) =>
_watermarks = await _mediator.Send(new GetAllWatermarks()); _watermarks = await _mediator.Send(new GetAllWatermarks(), cancellationToken);
private async Task LoadFillerPresets() => private async Task LoadFillerPresets(CancellationToken cancellationToken) =>
_fillerPresets = await _mediator.Send(new GetAllFillerPresets()) _fillerPresets = await _mediator.Send(new GetAllFillerPresets(), cancellationToken)
.Map(list => list.Filter(vm => vm.FillerKind == FillerKind.Fallback).ToList()); .Map(list => list.Filter(vm => vm.FillerKind == FillerKind.Fallback).ToList());
private async Task HandleSubmitAsync() private async Task HandleSubmitAsync()
@ -187,8 +196,8 @@
if (_editContext.Validate()) if (_editContext.Validate())
{ {
Seq<BaseError> errorMessage = IsEdit ? Seq<BaseError> errorMessage = IsEdit ?
(await _mediator.Send(_model.ToUpdate())).LeftToSeq() : (await _mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() :
(await _mediator.Send(_model.ToCreate())).LeftToSeq(); (await _mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
errorMessage.HeadOrNone().Match( errorMessage.HeadOrNone().Match(
error => error =>
@ -205,7 +214,7 @@
try try
{ {
Either<BaseError, string> maybeCacheFileName = Either<BaseError, string> maybeCacheFileName =
await _mediator.Send(new SaveArtworkToDisk(e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024), ArtworkKind.Logo)); await _mediator.Send(new SaveArtworkToDisk(e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024), ArtworkKind.Logo), _cts.Token);
maybeCacheFileName.Match( maybeCacheFileName.Match(
relativeFileName => relativeFileName =>
{ {

20
ErsatzTV/Pages/Channels.razor

@ -7,6 +7,8 @@
@using ErsatzTV.Application.FFmpegProfiles @using ErsatzTV.Application.FFmpegProfiles
@using ErsatzTV.Application.FFmpegProfiles.Queries @using ErsatzTV.Application.FFmpegProfiles.Queries
@using System.Globalization @using System.Globalization
@using System.Threading
@implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IMediator _mediator @inject IMediator _mediator
@ -82,15 +84,23 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private MudTable<ChannelViewModel> _table; private MudTable<ChannelViewModel> _table;
private List<FFmpegProfileViewModel> _ffmpegProfiles; private List<FFmpegProfileViewModel> _ffmpegProfiles;
private int _rowsPerPage; private int _rowsPerPage;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles()); _ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles(), _cts.Token);
_rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.ChannelsPageSize)) _rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.ChannelsPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
} }
@ -103,16 +113,16 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _mediator.Send(new DeleteChannel(channel.Id)); await _mediator.Send(new DeleteChannel(channel.Id), _cts.Token);
await _table.ReloadServerData(); await _table.ReloadServerData();
} }
} }
private async Task<TableData<ChannelViewModel>> ServerReload(TableState state) private async Task<TableData<ChannelViewModel>> ServerReload(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.ChannelsPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.ChannelsPageSize, state.PageSize.ToString()), _cts.Token);
List<ChannelViewModel> channels = await _mediator.Send(new GetAllChannels()); List<ChannelViewModel> channels = await _mediator.Send(new GetAllChannels(), _cts.Token);
IOrderedEnumerable<ChannelViewModel> sorted = channels.OrderBy(c => decimal.Parse(c.Number)); IOrderedEnumerable<ChannelViewModel> sorted = channels.OrderBy(c => decimal.Parse(c.Number));
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);

17
ErsatzTV/Pages/CollectionEditor.razor

@ -3,6 +3,8 @@
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.MediaCollections.Queries @using ErsatzTV.Application.MediaCollections.Queries
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<CollectionEditor> _logger @inject ILogger<CollectionEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -29,19 +31,26 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
private readonly CollectionEditViewModel _model = new(); private readonly CollectionEditViewModel _model = new();
private EditContext _editContext; private EditContext _editContext;
private ValidationMessageStore _messageStore; private ValidationMessageStore _messageStore;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if (IsEdit) if (IsEdit)
{ {
Option<MediaCollectionViewModel> maybeCollection = await _mediator.Send(new GetCollectionById(Id)); Option<MediaCollectionViewModel> maybeCollection = await _mediator.Send(new GetCollectionById(Id), _cts.Token);
maybeCollection.IfSome(collection => maybeCollection.IfSome(collection =>
{ {
_model.Id = collection.Id; _model.Id = collection.Id;
@ -68,8 +77,8 @@
if (_editContext.Validate()) if (_editContext.Validate())
{ {
Seq<BaseError> errorMessage = IsEdit ? Seq<BaseError> errorMessage = IsEdit ?
(await _mediator.Send(new UpdateCollection(Id, _model.Name))).LeftToSeq() : (await _mediator.Send(new UpdateCollection(Id, _model.Name), _cts.Token)).LeftToSeq() :
(await _mediator.Send(new CreateCollection(_model.Name))).LeftToSeq(); (await _mediator.Send(new CreateCollection(_model.Name), _cts.Token)).LeftToSeq();
errorMessage.HeadOrNone().Match( errorMessage.HeadOrNone().Match(
error => error =>

6
ErsatzTV/Pages/CollectionItems.razor

@ -294,7 +294,7 @@
protected override async Task RefreshData() protected override async Task RefreshData()
{ {
Either<BaseError, CollectionCardResultsViewModel> maybeResult = Either<BaseError, CollectionCardResultsViewModel> maybeResult =
await Mediator.Send(new GetCollectionCards(Id)); await Mediator.Send(new GetCollectionCards(Id), CancellationToken);
maybeResult.Match( maybeResult.Match(
result => _data = result, result => _data = result,
@ -459,7 +459,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await Mediator.Send(request); await Mediator.Send(request, CancellationToken);
await RefreshData(); await RefreshData();
} }
} }
@ -468,7 +468,7 @@
{ {
_data.UseCustomPlaybackOrder = !_data.UseCustomPlaybackOrder; _data.UseCustomPlaybackOrder = !_data.UseCustomPlaybackOrder;
var request = new UpdateCollection(Id, _data.Name) { UseCustomPlaybackOrder = _data.UseCustomPlaybackOrder }; var request = new UpdateCollection(Id, _data.Name) { UseCustomPlaybackOrder = _data.UseCustomPlaybackOrder };
await Mediator.Send(request); await Mediator.Send(request, CancellationToken);
} }
} }

34
ErsatzTV/Pages/Collections.razor

@ -5,6 +5,8 @@
@using ErsatzTV.Application.Configuration.Queries @using ErsatzTV.Application.Configuration.Queries
@using ErsatzTV.Application.Configuration.Commands @using ErsatzTV.Application.Configuration.Commands
@using ErsatzTV.Extensions @using ErsatzTV.Extensions
@using System.Threading
@implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IMediator _mediator @inject IMediator _mediator
@ -134,6 +136,8 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private MudTable<MediaCollectionViewModel> _collectionsTable; private MudTable<MediaCollectionViewModel> _collectionsTable;
private MudTable<MultiCollectionViewModel> _multiCollectionsTable; private MudTable<MultiCollectionViewModel> _multiCollectionsTable;
private MudTable<SmartCollectionViewModel> _smartCollectionsTable; private MudTable<SmartCollectionViewModel> _smartCollectionsTable;
@ -141,16 +145,22 @@
private int _collectionsRowsPerPage; private int _collectionsRowsPerPage;
private int _multiCollectionsRowsPerPage; private int _multiCollectionsRowsPerPage;
private int _smartCollectionsRowsPerPage; private int _smartCollectionsRowsPerPage;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_collectionsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.CollectionsPageSize)) _collectionsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.CollectionsPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_multiCollectionsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.MultiCollectionsPageSize)) _multiCollectionsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.MultiCollectionsPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_smartCollectionsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.SmartCollectionsPageSize)) _smartCollectionsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.SmartCollectionsPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
} }
@ -163,7 +173,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _mediator.Send(new DeleteCollection(collection.Id)); await _mediator.Send(new DeleteCollection(collection.Id), _cts.Token);
await _collectionsTable.ReloadServerData(); await _collectionsTable.ReloadServerData();
} }
} }
@ -177,7 +187,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _mediator.Send(new DeleteMultiCollection(collection.Id)); await _mediator.Send(new DeleteMultiCollection(collection.Id), _cts.Token);
await _multiCollectionsTable.ReloadServerData(); await _multiCollectionsTable.ReloadServerData();
} }
} }
@ -191,32 +201,32 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _mediator.Send(new DeleteSmartCollection(collection.Id)); await _mediator.Send(new DeleteSmartCollection(collection.Id), _cts.Token);
await _smartCollectionsTable.ReloadServerData(); await _smartCollectionsTable.ReloadServerData();
} }
} }
private async Task<TableData<MediaCollectionViewModel>> ServerReloadCollections(TableState state) private async Task<TableData<MediaCollectionViewModel>> ServerReloadCollections(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.CollectionsPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.CollectionsPageSize, state.PageSize.ToString()), _cts.Token);
PagedMediaCollectionsViewModel data = await _mediator.Send(new GetPagedCollections(state.Page, state.PageSize)); PagedMediaCollectionsViewModel data = await _mediator.Send(new GetPagedCollections(state.Page, state.PageSize), _cts.Token);
return new TableData<MediaCollectionViewModel> { TotalItems = data.TotalCount, Items = data.Page }; return new TableData<MediaCollectionViewModel> { TotalItems = data.TotalCount, Items = data.Page };
} }
private async Task<TableData<MultiCollectionViewModel>> ServerReloadMultiCollections(TableState state) private async Task<TableData<MultiCollectionViewModel>> ServerReloadMultiCollections(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.MultiCollectionsPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.MultiCollectionsPageSize, state.PageSize.ToString()), _cts.Token);
PagedMultiCollectionsViewModel data = await _mediator.Send(new GetPagedMultiCollections(state.Page, state.PageSize)); PagedMultiCollectionsViewModel data = await _mediator.Send(new GetPagedMultiCollections(state.Page, state.PageSize), _cts.Token);
return new TableData<MultiCollectionViewModel> { TotalItems = data.TotalCount, Items = data.Page }; return new TableData<MultiCollectionViewModel> { TotalItems = data.TotalCount, Items = data.Page };
} }
private async Task<TableData<SmartCollectionViewModel>> ServerReloadSmartCollections(TableState state) private async Task<TableData<SmartCollectionViewModel>> ServerReloadSmartCollections(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SmartCollectionsPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SmartCollectionsPageSize, state.PageSize.ToString()), _cts.Token);
PagedSmartCollectionsViewModel data = await _mediator.Send(new GetPagedSmartCollections(state.Page, state.PageSize)); PagedSmartCollectionsViewModel data = await _mediator.Send(new GetPagedSmartCollections(state.Page, state.PageSize), _cts.Token);
return new TableData<SmartCollectionViewModel> { TotalItems = data.TotalCount, Items = data.Page }; return new TableData<SmartCollectionViewModel> { TotalItems = data.TotalCount, Items = data.Page };
} }

11
ErsatzTV/Pages/EmbyLibrariesEditor.razor

@ -4,6 +4,8 @@
@using ErsatzTV.Application.Emby.Queries @using ErsatzTV.Application.Emby.Queries
@using ErsatzTV.Application.MediaSources @using ErsatzTV.Application.MediaSources
@using ErsatzTV.Application.Emby @using ErsatzTV.Application.Emby
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject ChannelWriter<IEmbyBackgroundServiceRequest> _channel @inject ChannelWriter<IEmbyBackgroundServiceRequest> _channel
@ -16,10 +18,17 @@
SynchronizeLibraryByIdIfNeeded="SynchronizeLibraryByIdIfNeeded"/> SynchronizeLibraryByIdIfNeeded="SynchronizeLibraryByIdIfNeeded"/>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private IRequest<Either<BaseError, Unit>> GetUpdateLibraryRequest(List<RemoteMediaSourceLibraryEditViewModel> libraries) => private IRequest<Either<BaseError, Unit>> GetUpdateLibraryRequest(List<RemoteMediaSourceLibraryEditViewModel> libraries) =>
new UpdateEmbyLibraryPreferences( new UpdateEmbyLibraryPreferences(
libraries.Map(l => new EmbyLibraryPreference(l.Id, l.ShouldSyncItems)).ToList()); libraries.Map(l => new EmbyLibraryPreference(l.Id, l.ShouldSyncItems)).ToList());
@ -42,7 +51,7 @@
private async Task<Unit> SynchronizeLibraryByIdIfNeeded(int libraryId) private async Task<Unit> SynchronizeLibraryByIdIfNeeded(int libraryId)
{ {
await _channel.WriteAsync(new SynchronizeEmbyLibraryByIdIfNeeded(libraryId)); await _channel.WriteAsync(new SynchronizeEmbyLibraryByIdIfNeeded(libraryId), _cts.Token);
return Unit.Default; return Unit.Default;
} }

13
ErsatzTV/Pages/EmbyMediaSourceEditor.razor

@ -3,6 +3,8 @@
@using ErsatzTV.Core.Emby @using ErsatzTV.Core.Emby
@using ErsatzTV.Application.Emby.Queries @using ErsatzTV.Application.Emby.Queries
@using ErsatzTV.Application.Emby.Commands @using ErsatzTV.Application.Emby.Commands
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -14,10 +16,17 @@
SaveSecrets="SaveSecrets"/> SaveSecrets="SaveSecrets"/>
@code { @code {
private readonly CancellationTokenSource _cts = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private async Task<Unit> LoadSecrets(RemoteMediaSourceEditViewModel viewModel) private async Task<Unit> LoadSecrets(RemoteMediaSourceEditViewModel viewModel)
{ {
EmbySecrets secrets = await _mediator.Send(new GetEmbySecrets()); EmbySecrets secrets = await _mediator.Send(new GetEmbySecrets(), _cts.Token);
viewModel.Address = secrets.Address; viewModel.Address = secrets.Address;
viewModel.ApiKey = secrets.ApiKey; viewModel.ApiKey = secrets.ApiKey;
return Unit.Default; return Unit.Default;
@ -26,7 +35,7 @@
private async Task<Either<BaseError, Unit>> SaveSecrets(RemoteMediaSourceEditViewModel viewModel) private async Task<Either<BaseError, Unit>> SaveSecrets(RemoteMediaSourceEditViewModel viewModel)
{ {
var secrets = new EmbySecrets { Address = viewModel.Address, ApiKey = viewModel.ApiKey }; var secrets = new EmbySecrets { Address = viewModel.Address, ApiKey = viewModel.ApiKey };
return await _mediator.Send(new SaveEmbySecrets(secrets)); return await _mediator.Send(new SaveEmbySecrets(secrets), _cts.Token);
} }
} }

11
ErsatzTV/Pages/EmbyMediaSources.razor

@ -2,6 +2,8 @@
@using ErsatzTV.Core.Interfaces.Emby @using ErsatzTV.Core.Interfaces.Emby
@using ErsatzTV.Application.Emby.Commands @using ErsatzTV.Application.Emby.Commands
@using ErsatzTV.Application.Emby.Queries @using ErsatzTV.Application.Emby.Queries
@using System.Threading
@implements IDisposable
@inject IEmbySecretStore _embySecretStore @inject IEmbySecretStore _embySecretStore
@inject ChannelWriter<IEmbyBackgroundServiceRequest> _channel @inject ChannelWriter<IEmbyBackgroundServiceRequest> _channel
@ -16,8 +18,15 @@
SecretStore="@_embySecretStore"/> SecretStore="@_embySecretStore"/>
@code { @code {
private readonly CancellationTokenSource _cts = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private async Task RefreshLibraries(int mediaSourceId) => private async Task RefreshLibraries(int mediaSourceId) =>
await _channel.WriteAsync(new SynchronizeEmbyLibraries(mediaSourceId)); await _channel.WriteAsync(new SynchronizeEmbyLibraries(mediaSourceId), _cts.Token);
} }

13
ErsatzTV/Pages/EmbyPathReplacementsEditor.razor

@ -4,6 +4,8 @@
@using ErsatzTV.Application.Emby @using ErsatzTV.Application.Emby
@using ErsatzTV.Application.Emby.Commands @using ErsatzTV.Application.Emby.Commands
@using Unit = LanguageExt.Unit @using Unit = LanguageExt.Unit
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<ScheduleItemsEditor> _logger @inject ILogger<ScheduleItemsEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -17,16 +19,23 @@
GetPathReplacementsBySourceId="GetPathReplacementsBySourceId"/> GetPathReplacementsBySourceId="GetPathReplacementsBySourceId"/>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id) => private Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id) =>
_mediator.Send(new GetEmbyMediaSourceById(Id)) _mediator.Send(new GetEmbyMediaSourceById(Id), _cts.Token)
.MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address)); .MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address));
private Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId) => private Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId) =>
_mediator.Send(new GetEmbyPathReplacementsBySourceId(Id)) _mediator.Send(new GetEmbyPathReplacementsBySourceId(Id), _cts.Token)
.Map(list => list.Map(ProjectToEditViewModel).ToList()); .Map(list => list.Map(ProjectToEditViewModel).ToList());
private RemoteMediaSourcePathReplacementEditViewModel ProjectToEditViewModel(EmbyPathReplacementViewModel item) => private RemoteMediaSourcePathReplacementEditViewModel ProjectToEditViewModel(EmbyPathReplacementViewModel item) =>

4
ErsatzTV/Pages/EpisodeList.razor

@ -97,7 +97,7 @@
protected override async Task RefreshData() protected override async Task RefreshData()
{ {
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:episode" : $"type:episode AND ({_query})"; string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:episode" : $"type:episode AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexEpisodes(searchQuery, PageNumber, PageSize)); _data = await Mediator.Send(new QuerySearchIndexEpisodes(searchQuery, PageNumber, PageSize), CancellationToken);
} }
private void PrevPage() private void PrevPage()
@ -144,7 +144,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId); var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

20
ErsatzTV/Pages/FFmpeg.razor

@ -2,6 +2,8 @@
@using ErsatzTV.Application.FFmpegProfiles @using ErsatzTV.Application.FFmpegProfiles
@using ErsatzTV.Application.FFmpegProfiles.Commands @using ErsatzTV.Application.FFmpegProfiles.Commands
@using ErsatzTV.Application.FFmpegProfiles.Queries @using ErsatzTV.Application.FFmpegProfiles.Queries
@using System.Threading
@implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IMediator _mediator @inject IMediator _mediator
@inject ILogger<FFmpeg> _logger @inject ILogger<FFmpeg> _logger
@ -78,12 +80,20 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private List<FFmpegProfileViewModel> _ffmpegProfiles; private List<FFmpegProfileViewModel> _ffmpegProfiles;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadFFmpegProfilesAsync(); protected override async Task OnParametersSetAsync() => await LoadFFmpegProfilesAsync(_cts.Token);
private async Task LoadFFmpegProfilesAsync() => private async Task LoadFFmpegProfilesAsync(CancellationToken cancellationToken) =>
_ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles()); _ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles(), cancellationToken);
private async Task DeleteProfileAsync(FFmpegProfileViewModel ffmpegProfile) private async Task DeleteProfileAsync(FFmpegProfileViewModel ffmpegProfile)
{ {
@ -94,8 +104,8 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _mediator.Send(new DeleteFFmpegProfile(ffmpegProfile.Id)); await _mediator.Send(new DeleteFFmpegProfile(ffmpegProfile.Id), _cts.Token);
await LoadFFmpegProfilesAsync(); await LoadFFmpegProfilesAsync(_cts.Token);
} }
} }

19
ErsatzTV/Pages/FFmpegEditor.razor

@ -8,6 +8,8 @@
@using ErsatzTV.Core.FFmpeg @using ErsatzTV.Core.FFmpeg
@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components
@using Microsoft.Extensions.Caching.Memory @using Microsoft.Extensions.Caching.Memory
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<FFmpegEditor> _logger @inject ILogger<FFmpegEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -121,6 +123,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -132,20 +135,26 @@
private List<ResolutionViewModel> _resolutions; private List<ResolutionViewModel> _resolutions;
private List<string> _vaapiDevices; private List<string> _vaapiDevices;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_resolutions = await _mediator.Send(new GetAllResolutions()); _resolutions = await _mediator.Send(new GetAllResolutions(), _cts.Token);
if (IsEdit) if (IsEdit)
{ {
Option<FFmpegProfileViewModel> profile = await _mediator.Send(new GetFFmpegProfileById(Id)); Option<FFmpegProfileViewModel> profile = await _mediator.Send(new GetFFmpegProfileById(Id), _cts.Token);
profile.Match( profile.Match(
ffmpegProfileViewModel => _model = new FFmpegProfileEditViewModel(ffmpegProfileViewModel), ffmpegProfileViewModel => _model = new FFmpegProfileEditViewModel(ffmpegProfileViewModel),
() => _navigationManager.NavigateTo("404")); () => _navigationManager.NavigateTo("404"));
} }
else else
{ {
_model = new FFmpegProfileEditViewModel(await _mediator.Send(new NewFFmpegProfile())); _model = new FFmpegProfileEditViewModel(await _mediator.Send(new NewFFmpegProfile(), _cts.Token));
} }
_editContext = new EditContext(_model); _editContext = new EditContext(_model);
@ -167,8 +176,8 @@
if (_editContext.Validate()) if (_editContext.Validate())
{ {
Seq<BaseError> errorMessage = IsEdit ? Seq<BaseError> errorMessage = IsEdit ?
(await _mediator.Send(_model.ToUpdate())).LeftToSeq() : (await _mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() :
(await _mediator.Send(_model.ToCreate())).LeftToSeq(); (await _mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
errorMessage.HeadOrNone().Match( errorMessage.HeadOrNone().Match(
error => error =>

26
ErsatzTV/Pages/FillerPresetEditor.razor

@ -9,6 +9,8 @@
@using ErsatzTV.Application.MediaItems @using ErsatzTV.Application.MediaItems
@using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Core.Domain.Filler
@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<FillerPresetEditor> _logger @inject ILogger<FillerPresetEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -134,6 +136,8 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -148,24 +152,30 @@
private List<NamedMediaItemViewModel> _televisionSeasons; private List<NamedMediaItemViewModel> _televisionSeasons;
private List<NamedMediaItemViewModel> _artists; private List<NamedMediaItemViewModel> _artists;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_mediaCollections = await _mediator.Send(new GetAllCollections()) _mediaCollections = await _mediator.Send(new GetAllCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_multiCollections = await _mediator.Send(new GetAllMultiCollections()) _multiCollections = await _mediator.Send(new GetAllMultiCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_smartCollections = await _mediator.Send(new GetAllSmartCollections()) _smartCollections = await _mediator.Send(new GetAllSmartCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_televisionShows = await _mediator.Send(new GetAllTelevisionShows()) _televisionShows = await _mediator.Send(new GetAllTelevisionShows(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_televisionSeasons = await _mediator.Send(new GetAllTelevisionSeasons()) _televisionSeasons = await _mediator.Send(new GetAllTelevisionSeasons(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_artists = await _mediator.Send(new GetAllArtists()) _artists = await _mediator.Send(new GetAllArtists(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
if (IsEdit) if (IsEdit)
{ {
Option<FillerPresetViewModel> maybeFillerPreset = await _mediator.Send(new GetFillerPresetById(Id)); Option<FillerPresetViewModel> maybeFillerPreset = await _mediator.Send(new GetFillerPresetById(Id), _cts.Token);
maybeFillerPreset.IfSome(fillerPreset => maybeFillerPreset.IfSome(fillerPreset =>
{ {
_model.Id = fillerPreset.Id; _model.Id = fillerPreset.Id;
@ -212,7 +222,7 @@
{ {
IRequest<Either<BaseError, LanguageExt.Unit>> request = IsEdit ? _model.ToEdit() : _model.ToUpdate(); IRequest<Either<BaseError, LanguageExt.Unit>> request = IsEdit ? _model.ToEdit() : _model.ToUpdate();
Seq<BaseError> errorMessage = await _mediator.Send(request) Seq<BaseError> errorMessage = await _mediator.Send(request, _cts.Token)
.Map(result => result.LeftToSeq()); .Map(result => result.LeftToSeq());
errorMessage.HeadOrNone().Match( errorMessage.HeadOrNone().Match(

19
ErsatzTV/Pages/FillerPresets.razor

@ -5,6 +5,8 @@
@using ErsatzTV.Application.Filler.Commands @using ErsatzTV.Application.Filler.Commands
@using ErsatzTV.Application.Filler.Queries @using ErsatzTV.Application.Filler.Queries
@using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Core.Domain.Filler
@using System.Threading
@implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IMediator _mediator @inject IMediator _mediator
@ -69,13 +71,21 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private MudTable<FillerPresetViewModel> _fillerPresetsTable; private MudTable<FillerPresetViewModel> _fillerPresetsTable;
private int _fillerPresetsRowsPerPage; private int _fillerPresetsRowsPerPage;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_fillerPresetsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.FillerPresetsPageSize)) _fillerPresetsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.FillerPresetsPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
} }
@ -88,16 +98,17 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _mediator.Send(new DeleteFillerPreset(fillerPreset.Id)); await _mediator.Send(new DeleteFillerPreset(fillerPreset.Id), _cts.Token);
await _fillerPresetsTable.ReloadServerData(); await _fillerPresetsTable.ReloadServerData();
} }
} }
private async Task<TableData<FillerPresetViewModel>> ServerReloadFillerPresets(TableState state) private async Task<TableData<FillerPresetViewModel>> ServerReloadFillerPresets(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.FillerPresetsPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.FillerPresetsPageSize, state.PageSize.ToString()), _cts.Token);
PagedFillerPresetsViewModel data = await _mediator.Send(new GetPagedFillerPresets(state.Page, state.PageSize)); PagedFillerPresetsViewModel data = await _mediator.Send(new GetPagedFillerPresets(state.Page, state.PageSize), _cts.Token);
return new TableData<FillerPresetViewModel> { TotalItems = data.TotalCount, Items = data.Page }; return new TableData<FillerPresetViewModel> { TotalItems = data.TotalCount, Items = data.Page };
} }
} }

13
ErsatzTV/Pages/FragmentNavigationBase.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Extensions; using ErsatzTV.Extensions;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -10,13 +11,23 @@ namespace ErsatzTV.Pages
{ {
public class FragmentNavigationBase : ComponentBase, IDisposable public class FragmentNavigationBase : ComponentBase, IDisposable
{ {
private readonly CancellationTokenSource _cts = new();
protected CancellationToken CancellationToken => _cts.Token;
[Inject] [Inject]
private NavigationManager NavManager { get; set; } private NavigationManager NavManager { get; set; }
[Inject] [Inject]
private IJSRuntime JsRuntime { get; set; } private IJSRuntime JsRuntime { get; set; }
public void Dispose() => NavManager.LocationChanged -= TryFragmentNavigation; public void Dispose()
{
NavManager.LocationChanged -= TryFragmentNavigation;
_cts?.Cancel();
_cts?.Dispose();
}
protected override void OnInitialized() => NavManager.LocationChanged += TryFragmentNavigation; protected override void OnInitialized() => NavManager.LocationChanged += TryFragmentNavigation;

15
ErsatzTV/Pages/Index.razor

@ -1,9 +1,11 @@
@page "/" @page "/"
@using Microsoft.Extensions.Caching.Memory @using Microsoft.Extensions.Caching.Memory
@using System.Reflection @using System.Reflection
@using System.Threading
@using ErsatzTV.Application.Health.Queries @using ErsatzTV.Application.Health.Queries
@using ErsatzTV.Core.Health @using ErsatzTV.Core.Health
@using ErsatzTV.Core.Interfaces.GitHub @using ErsatzTV.Core.Interfaces.GitHub
@implements IDisposable
@inject IGitHubApiClient _gitHubApiClient @inject IGitHubApiClient _gitHubApiClient
@inject IMemoryCache _memoryCache @inject IMemoryCache _memoryCache
@inject IMediator _mediator @inject IMediator _mediator
@ -71,9 +73,16 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private string _releaseNotes; private string _releaseNotes;
private MudTable<HealthCheckResult> _table; private MudTable<HealthCheckResult> _table;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
@ -101,12 +110,12 @@
gitHubVersion = $"v{gitHubVersion}"; gitHubVersion = $"v{gitHubVersion}";
} }
maybeNotes = await _gitHubApiClient.GetReleaseNotes(gitHubVersion); maybeNotes = await _gitHubApiClient.GetReleaseNotes(gitHubVersion, _cts.Token);
maybeNotes.IfRight(notes => _releaseNotes = notes); maybeNotes.IfRight(notes => _releaseNotes = notes);
} }
else else
{ {
maybeNotes = await _gitHubApiClient.GetLatestReleaseNotes(); maybeNotes = await _gitHubApiClient.GetLatestReleaseNotes(_cts.Token);
maybeNotes.IfRight(notes => _releaseNotes = notes); maybeNotes.IfRight(notes => _releaseNotes = notes);
} }
} }
@ -126,7 +135,7 @@
private async Task<TableData<HealthCheckResult>> ServerReload(TableState state) private async Task<TableData<HealthCheckResult>> ServerReload(TableState state)
{ {
List<HealthCheckResult> healthCheckResults = await _mediator.Send(new GetAllHealthCheckResults()); List<HealthCheckResult> healthCheckResults = await _mediator.Send(new GetAllHealthCheckResults(), _cts.Token);
return new TableData<HealthCheckResult> return new TableData<HealthCheckResult>
{ {

19
ErsatzTV/Pages/JellyfinLibrariesEditor.razor

@ -4,6 +4,8 @@
@using ErsatzTV.Application.Jellyfin.Queries @using ErsatzTV.Application.Jellyfin.Queries
@using ErsatzTV.Application.MediaSources @using ErsatzTV.Application.MediaSources
@using ErsatzTV.Application.Jellyfin @using ErsatzTV.Application.Jellyfin
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject ChannelWriter<IJellyfinBackgroundServiceRequest> _channel @inject ChannelWriter<IJellyfinBackgroundServiceRequest> _channel
@ -16,20 +18,27 @@
SynchronizeLibraryByIdIfNeeded="SynchronizeLibraryByIdIfNeeded"/> SynchronizeLibraryByIdIfNeeded="SynchronizeLibraryByIdIfNeeded"/>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private IRequest<Either<BaseError, Unit>> GetUpdateLibraryRequest(List<RemoteMediaSourceLibraryEditViewModel> libraries) => private IRequest<Either<BaseError, Unit>> GetUpdateLibraryRequest(List<RemoteMediaSourceLibraryEditViewModel> libraries) =>
new UpdateJellyfinLibraryPreferences( new UpdateJellyfinLibraryPreferences(
libraries.Map(l => new JellyfinLibraryPreference(l.Id, l.ShouldSyncItems)).ToList()); libraries.Map(l => new JellyfinLibraryPreference(l.Id, l.ShouldSyncItems)).ToList());
private Task<List<RemoteMediaSourceLibraryEditViewModel>> GetLibrariesBySourceId(int mediaSourceId) => private async Task<List<RemoteMediaSourceLibraryEditViewModel>> GetLibrariesBySourceId(int mediaSourceId) =>
_mediator.Send(new GetJellyfinLibrariesBySourceId(Id)) await _mediator.Send(new GetJellyfinLibrariesBySourceId(Id), _cts.Token)
.Map(list => list.Map(ProjectToEditViewModel).OrderBy(x => x.MediaKind).ThenBy(x => x.Name).ToList()); .Map(list => list.Map(ProjectToEditViewModel).OrderBy(x => x.MediaKind).ThenBy(x => x.Name).ToList());
private Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int mediaSourceId) => private async Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int mediaSourceId) =>
_mediator.Send(new GetJellyfinMediaSourceById(Id)) await _mediator.Send(new GetJellyfinMediaSourceById(Id), _cts.Token)
.MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address)); .MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address));
private RemoteMediaSourceLibraryEditViewModel ProjectToEditViewModel(JellyfinLibraryViewModel library) => new() private RemoteMediaSourceLibraryEditViewModel ProjectToEditViewModel(JellyfinLibraryViewModel library) => new()
@ -42,7 +51,7 @@
private async Task<Unit> SynchronizeLibraryByIdIfNeeded(int libraryId) private async Task<Unit> SynchronizeLibraryByIdIfNeeded(int libraryId)
{ {
await _channel.WriteAsync(new SynchronizeJellyfinLibraryByIdIfNeeded(libraryId)); await _channel.WriteAsync(new SynchronizeJellyfinLibraryByIdIfNeeded(libraryId), _cts.Token);
return Unit.Default; return Unit.Default;
} }

13
ErsatzTV/Pages/JellyfinMediaSourceEditor.razor

@ -3,6 +3,8 @@
@using ErsatzTV.Core.Jellyfin @using ErsatzTV.Core.Jellyfin
@using ErsatzTV.Application.Jellyfin.Queries @using ErsatzTV.Application.Jellyfin.Queries
@using ErsatzTV.Application.Jellyfin.Commands @using ErsatzTV.Application.Jellyfin.Commands
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -14,10 +16,17 @@
SaveSecrets="SaveSecrets"/> SaveSecrets="SaveSecrets"/>
@code { @code {
private readonly CancellationTokenSource _cts = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private async Task<Unit> LoadSecrets(RemoteMediaSourceEditViewModel viewModel) private async Task<Unit> LoadSecrets(RemoteMediaSourceEditViewModel viewModel)
{ {
JellyfinSecrets secrets = await _mediator.Send(new GetJellyfinSecrets()); JellyfinSecrets secrets = await _mediator.Send(new GetJellyfinSecrets(), _cts.Token);
viewModel.Address = secrets.Address; viewModel.Address = secrets.Address;
viewModel.ApiKey = secrets.ApiKey; viewModel.ApiKey = secrets.ApiKey;
return Unit.Default; return Unit.Default;
@ -26,7 +35,7 @@
private async Task<Either<BaseError, Unit>> SaveSecrets(RemoteMediaSourceEditViewModel viewModel) private async Task<Either<BaseError, Unit>> SaveSecrets(RemoteMediaSourceEditViewModel viewModel)
{ {
var secrets = new JellyfinSecrets { Address = viewModel.Address, ApiKey = viewModel.ApiKey }; var secrets = new JellyfinSecrets { Address = viewModel.Address, ApiKey = viewModel.ApiKey };
return await _mediator.Send(new SaveJellyfinSecrets(secrets)); return await _mediator.Send(new SaveJellyfinSecrets(secrets), _cts.Token);
} }
} }

11
ErsatzTV/Pages/JellyfinMediaSources.razor

@ -2,6 +2,8 @@
@using ErsatzTV.Core.Interfaces.Jellyfin @using ErsatzTV.Core.Interfaces.Jellyfin
@using ErsatzTV.Application.Jellyfin.Commands @using ErsatzTV.Application.Jellyfin.Commands
@using ErsatzTV.Application.Jellyfin.Queries @using ErsatzTV.Application.Jellyfin.Queries
@using System.Threading
@implements IDisposable
@inject IJellyfinSecretStore _jellyfinSecretStore @inject IJellyfinSecretStore _jellyfinSecretStore
@inject ChannelWriter<IJellyfinBackgroundServiceRequest> _channel @inject ChannelWriter<IJellyfinBackgroundServiceRequest> _channel
@ -16,8 +18,15 @@
SecretStore="@_jellyfinSecretStore"/> SecretStore="@_jellyfinSecretStore"/>
@code { @code {
private readonly CancellationTokenSource _cts = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private async Task RefreshLibraries(int mediaSourceId) => private async Task RefreshLibraries(int mediaSourceId) =>
await _channel.WriteAsync(new SynchronizeJellyfinLibraries(mediaSourceId)); await _channel.WriteAsync(new SynchronizeJellyfinLibraries(mediaSourceId), _cts.Token);
} }

17
ErsatzTV/Pages/JellyfinPathReplacementsEditor.razor

@ -4,6 +4,8 @@
@using ErsatzTV.Application.Jellyfin @using ErsatzTV.Application.Jellyfin
@using ErsatzTV.Application.Jellyfin.Commands @using ErsatzTV.Application.Jellyfin.Commands
@using Unit = LanguageExt.Unit @using Unit = LanguageExt.Unit
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<ScheduleItemsEditor> _logger @inject ILogger<ScheduleItemsEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -17,16 +19,23 @@
GetPathReplacementsBySourceId="GetPathReplacementsBySourceId"/> GetPathReplacementsBySourceId="GetPathReplacementsBySourceId"/>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
private Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id) => public void Dispose()
_mediator.Send(new GetJellyfinMediaSourceById(Id)) {
_cts.Cancel();
_cts.Dispose();
}
private async Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id) =>
await _mediator.Send(new GetJellyfinMediaSourceById(Id), _cts.Token)
.MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address)); .MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address));
private Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId) => private async Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId) =>
_mediator.Send(new GetJellyfinPathReplacementsBySourceId(Id)) await _mediator.Send(new GetJellyfinPathReplacementsBySourceId(Id), _cts.Token)
.Map(list => list.Map(ProjectToEditViewModel).ToList()); .Map(list => list.Map(ProjectToEditViewModel).ToList());
private RemoteMediaSourcePathReplacementEditViewModel ProjectToEditViewModel(JellyfinPathReplacementViewModel item) => private RemoteMediaSourcePathReplacementEditViewModel ProjectToEditViewModel(JellyfinPathReplacementViewModel item) =>

19
ErsatzTV/Pages/Libraries.razor

@ -78,6 +78,8 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private IList<LibraryViewModel> _libraries; private IList<LibraryViewModel> _libraries;
private Dictionary<int, int> _progressByLibrary; private Dictionary<int, int> _progressByLibrary;
@ -87,11 +89,11 @@
_courier.Subscribe<LibraryScanProgress>(HandleScanProgress); _courier.Subscribe<LibraryScanProgress>(HandleScanProgress);
} }
protected override async Task OnParametersSetAsync() => await LoadLibraries(); protected override async Task OnParametersSetAsync() => await LoadLibraries(_cts.Token);
private async Task LoadLibraries() private async Task LoadLibraries(CancellationToken cancellationToken)
{ {
_libraries = await _mediator.Send(new GetConfiguredLibraries()); _libraries = await _mediator.Send(new GetConfiguredLibraries(), cancellationToken);
_progressByLibrary = _libraries.ToDictionary(vm => vm.Id, _ => 0); _progressByLibrary = _libraries.ToDictionary(vm => vm.Id, _ => 0);
} }
@ -102,16 +104,16 @@
switch (library) switch (library)
{ {
case LocalLibraryViewModel: case LocalLibraryViewModel:
await _workerChannel.WriteAsync(new ForceScanLocalLibrary(library.Id)); await _workerChannel.WriteAsync(new ForceScanLocalLibrary(library.Id), _cts.Token);
break; break;
case PlexLibraryViewModel: case PlexLibraryViewModel:
await _plexWorkerChannel.WriteAsync(new ForceSynchronizePlexLibraryById(library.Id)); await _plexWorkerChannel.WriteAsync(new ForceSynchronizePlexLibraryById(library.Id), _cts.Token);
break; break;
case JellyfinLibraryViewModel: case JellyfinLibraryViewModel:
await _jellyfinWorkerChannel.WriteAsync(new ForceSynchronizeJellyfinLibraryById(library.Id)); await _jellyfinWorkerChannel.WriteAsync(new ForceSynchronizeJellyfinLibraryById(library.Id), _cts.Token);
break; break;
case EmbyLibraryViewModel: case EmbyLibraryViewModel:
await _embyWorkerChannel.WriteAsync(new ForceSynchronizeEmbyLibraryById(library.Id)); await _embyWorkerChannel.WriteAsync(new ForceSynchronizeEmbyLibraryById(library.Id), _cts.Token);
break; break;
} }
@ -142,6 +144,9 @@
{ {
_locker.OnLibraryChanged -= LockChanged; _locker.OnLibraryChanged -= LockChanged;
_courier.UnSubscribe<LibraryScanProgress>(HandleScanProgress); _courier.UnSubscribe<LibraryScanProgress>(HandleScanProgress);
_cts.Cancel();
_cts.Dispose();
} }
} }

18
ErsatzTV/Pages/LocalLibraries.razor

@ -2,6 +2,7 @@
@using ErsatzTV.Application.Libraries @using ErsatzTV.Application.Libraries
@using ErsatzTV.Application.Libraries.Commands @using ErsatzTV.Application.Libraries.Commands
@using ErsatzTV.Application.Libraries.Queries @using ErsatzTV.Application.Libraries.Queries
@using System.Threading
@implements IDisposable @implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IMediator _mediator @inject IMediator _mediator
@ -53,6 +54,8 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private IList<LocalLibraryViewModel> _libraries; private IList<LocalLibraryViewModel> _libraries;
protected override void OnInitialized() protected override void OnInitialized()
@ -60,11 +63,11 @@
_locker.OnLibraryChanged += LockChanged; _locker.OnLibraryChanged += LockChanged;
} }
protected override async Task OnParametersSetAsync() => await LoadLibraries(); protected override async Task OnParametersSetAsync() => await LoadLibraries(_cts.Token);
private async Task LoadLibraries() private async Task LoadLibraries(CancellationToken cancellationToken)
{ {
_libraries = await _mediator.Send(new GetAllLocalLibraries()); _libraries = await _mediator.Send(new GetAllLocalLibraries(), cancellationToken);
} }
private void LockChanged(object sender, EventArgs e) => private void LockChanged(object sender, EventArgs e) =>
@ -72,7 +75,7 @@
private async Task DeleteLibrary(LocalLibraryViewModel library) private async Task DeleteLibrary(LocalLibraryViewModel library)
{ {
int count = await _mediator.Send(new CountMediaItemsByLibrary(library.Id)); int count = await _mediator.Send(new CountMediaItemsByLibrary(library.Id), _cts.Token);
var parameters = new DialogParameters var parameters = new DialogParameters
{ {
{ "EntityType", "library" }, { "EntityType", "library" },
@ -86,14 +89,17 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _mediator.Send(new DeleteLocalLibrary(library.Id)); await _mediator.Send(new DeleteLocalLibrary(library.Id), _cts.Token);
await LoadLibraries(); await LoadLibraries(_cts.Token);
} }
} }
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
_locker.OnLibraryChanged -= LockChanged; _locker.OnLibraryChanged -= LockChanged;
_cts.Cancel();
_cts.Dispose();
} }
} }

17
ErsatzTV/Pages/LocalLibraryEditor.razor

@ -4,6 +4,7 @@
@using ErsatzTV.Application.Libraries.Commands @using ErsatzTV.Application.Libraries.Commands
@using ErsatzTV.Application.Libraries.Queries @using ErsatzTV.Application.Libraries.Queries
@using Unit = LanguageExt.Unit @using Unit = LanguageExt.Unit
@using System.Threading
@implements IDisposable @implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IEntityLocker _locker @inject IEntityLocker _locker
@ -76,6 +77,7 @@
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -91,7 +93,7 @@
{ {
if (IsEdit) if (IsEdit)
{ {
Option<LocalLibraryViewModel> maybeLibrary = await _mediator.Send(new GetLocalLibraryById(Id)); Option<LocalLibraryViewModel> maybeLibrary = await _mediator.Send(new GetLocalLibraryById(Id), _cts.Token);
await maybeLibrary.Match( await maybeLibrary.Match(
async library => async library =>
{ {
@ -130,7 +132,7 @@
private async Task LoadLibraryPaths() private async Task LoadLibraryPaths()
{ {
_model.HasChanges = false; _model.HasChanges = false;
_model.Paths = await _mediator.Send(new GetLocalLibraryPaths(Id)) _model.Paths = await _mediator.Send(new GetLocalLibraryPaths(Id), _cts.Token)
.Map(list => list.Map(vm => new LocalLibraryPathEditViewModel .Map(list => list.Map(vm => new LocalLibraryPathEditViewModel
{ {
Id = vm.Id, Id = vm.Id,
@ -148,7 +150,7 @@
if (!result.Cancelled && result.Data is LocalLibraryViewModel library) if (!result.Cancelled && result.Data is LocalLibraryViewModel library)
{ {
var request = new MoveLocalLibraryPath(libraryPath.Id, library.Id); var request = new MoveLocalLibraryPath(libraryPath.Id, library.Id);
Either<BaseError, Unit> moveResult = await _mediator.Send(request); Either<BaseError, Unit> moveResult = await _mediator.Send(request, _cts.Token);
moveResult.Match( moveResult.Match(
_ => _navigationManager.NavigateTo($"/media/sources/local/{library.Id}/edit"), _ => _navigationManager.NavigateTo($"/media/sources/local/{library.Id}/edit"),
error => error =>
@ -167,7 +169,7 @@
_model.Paths.Remove(libraryPath); _model.Paths.Remove(libraryPath);
} }
int count = await _mediator.Send(new CountMediaItemsByLibraryPath(libraryPath.Id)); int count = await _mediator.Send(new CountMediaItemsByLibraryPath(libraryPath.Id), _cts.Token);
var parameters = new DialogParameters var parameters = new DialogParameters
{ {
{ "EntityType", "library path" }, { "EntityType", "library path" },
@ -209,11 +211,11 @@
? await _mediator.Send(new UpdateLocalLibrary( ? await _mediator.Send(new UpdateLocalLibrary(
_model.Id, _model.Id,
_model.Name, _model.Name,
_model.Paths.Map(p => new UpdateLocalLibraryPath(p.Id, p.Path)).ToList())) _model.Paths.Map(p => new UpdateLocalLibraryPath(p.Id, p.Path)).ToList()), _cts.Token)
: await _mediator.Send(new CreateLocalLibrary( : await _mediator.Send(new CreateLocalLibrary(
_model.Name, _model.Name,
_model.MediaKind, _model.MediaKind,
_model.Paths.Map(p => p.Path).ToList())); _model.Paths.Map(p => p.Path).ToList()), _cts.Token);
result.Match( result.Match(
_ => _navigationManager.NavigateTo("/media/sources/local"), _ => _navigationManager.NavigateTo("/media/sources/local"),
@ -235,6 +237,9 @@
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
_locker.OnLibraryChanged -= LockChanged; _locker.OnLibraryChanged -= LockChanged;
_cts.Cancel();
_cts.Dispose();
} }
} }

15
ErsatzTV/Pages/LocalLibraryPathEditor.razor

@ -3,6 +3,8 @@
@using ErsatzTV.Application.Libraries.Commands @using ErsatzTV.Application.Libraries.Commands
@using ErsatzTV.Application.Libraries.Queries @using ErsatzTV.Application.Libraries.Queries
@using ErsatzTV.Application.MediaSources.Commands @using ErsatzTV.Application.MediaSources.Commands
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<LocalLibraryPathEditor> _logger @inject ILogger<LocalLibraryPathEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -32,6 +34,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -41,10 +44,16 @@
private ValidationMessageStore _messageStore; private ValidationMessageStore _messageStore;
private LocalLibraryViewModel _library; private LocalLibraryViewModel _library;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
Option<LocalLibraryViewModel> maybeLibrary = await _mediator.Send(new GetLocalLibraryById(Id)); Option<LocalLibraryViewModel> maybeLibrary = await _mediator.Send(new GetLocalLibraryById(Id), _cts.Token);
maybeLibrary.Match( maybeLibrary.Match(
library => _library = library, library => _library = library,
() => _navigationManager.NavigateTo("404")); () => _navigationManager.NavigateTo("404"));
@ -62,7 +71,7 @@
if (_editContext.Validate()) if (_editContext.Validate())
{ {
var command = new CreateLocalLibraryPath(_library.Id, _model.Path); var command = new CreateLocalLibraryPath(_library.Id, _model.Path);
Either<BaseError, LocalLibraryPathViewModel> result = await _mediator.Send(command); Either<BaseError, LocalLibraryPathViewModel> result = await _mediator.Send(command, _cts.Token);
await result.Match( await result.Match(
Left: error => Left: error =>
{ {
@ -74,7 +83,7 @@
{ {
if (_locker.LockLibrary(_library.Id)) if (_locker.LockLibrary(_library.Id))
{ {
await _channel.WriteAsync(new ScanLocalLibraryIfNeeded(_library.Id)); await _channel.WriteAsync(new ScanLocalLibraryIfNeeded(_library.Id), _cts.Token);
_navigationManager.NavigateTo("/media/libraries"); _navigationManager.NavigateTo("/media/libraries");
} }
}); });

21
ErsatzTV/Pages/Logs.razor

@ -3,6 +3,8 @@
@using ErsatzTV.Application.Logs.Queries @using ErsatzTV.Application.Logs.Queries
@using ErsatzTV.Application.Configuration.Queries @using ErsatzTV.Application.Configuration.Queries
@using ErsatzTV.Application.Configuration.Commands @using ErsatzTV.Application.Configuration.Commands
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
@ -36,15 +38,24 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private MudTable<LogEntryViewModel> _table; private MudTable<LogEntryViewModel> _table;
private int _rowsPerPage; private int _rowsPerPage;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() => _rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.LogsPageSize)) protected override async Task OnParametersSetAsync() => _rowsPerPage =
await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.LogsPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
private async Task<TableData<LogEntryViewModel>> ServerReload(TableState state) private async Task<TableData<LogEntryViewModel>> ServerReload(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.LogsPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.LogsPageSize, state.PageSize.ToString()), _cts.Token);
PagedLogEntriesViewModel data; PagedLogEntriesViewModel data;
@ -57,7 +68,7 @@
SortDescending = state.SortDirection == SortDirection.None SortDescending = state.SortDirection == SortDirection.None
? Option<bool>.None ? Option<bool>.None
: state.SortDirection == SortDirection.Descending : state.SortDirection == SortDirection.Descending
}); }, _cts.Token);
break; break;
case "level": case "level":
data = await _mediator.Send(new GetRecentLogEntries(state.Page, state.PageSize) data = await _mediator.Send(new GetRecentLogEntries(state.Page, state.PageSize)
@ -66,13 +77,13 @@
SortDescending = state.SortDirection == SortDirection.None SortDescending = state.SortDirection == SortDirection.None
? Option<bool>.None ? Option<bool>.None
: state.SortDirection == SortDirection.Descending : state.SortDirection == SortDirection.Descending
}); }, _cts.Token);
break; break;
default: default:
data = await _mediator.Send(new GetRecentLogEntries(state.Page, state.PageSize) data = await _mediator.Send(new GetRecentLogEntries(state.Page, state.PageSize)
{ {
SortDescending = Option<bool>.None SortDescending = Option<bool>.None
}); }, _cts.Token);
break; break;
} }

15
ErsatzTV/Pages/Movie.razor

@ -6,6 +6,8 @@
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Extensions @using ErsatzTV.Extensions
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject IDialogService _dialog @inject IDialogService _dialog
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@ -187,6 +189,7 @@
} }
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int MovieId { get; set; } public int MovieId { get; set; }
@ -200,10 +203,16 @@
private List<string> _sortedGenres = new(); private List<string> _sortedGenres = new();
private List<string> _sortedTags = new(); private List<string> _sortedTags = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override Task OnParametersSetAsync() => RefreshData(); protected override Task OnParametersSetAsync() => RefreshData();
private Task RefreshData() => private async Task RefreshData() =>
_mediator.Send(new GetMovieById(MovieId)).IfSomeAsync(vm => await _mediator.Send(new GetMovieById(MovieId), _cts.Token).IfSomeAsync(vm =>
{ {
_movie = vm; _movie = vm;
_sortedContentRatings = _movie.ContentRatings.OrderBy(cr => cr).ToList(); _sortedContentRatings = _movie.ContentRatings.OrderBy(cr => cr).ToList();
@ -224,7 +233,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
await _mediator.Send(new AddMovieToCollection(collection.Id, MovieId)); await _mediator.Send(new AddMovieToCollection(collection.Id, MovieId), _cts.Token);
_navigationManager.NavigateTo($"/media/collections/{collection.Id}"); _navigationManager.NavigateTo($"/media/collections/{collection.Id}");
} }
} }

11
ErsatzTV/Pages/MovieList.razor

@ -1,7 +1,6 @@
@page "/media/movies" @page "/media/movies"
@page "/media/movies/page/{PageNumber:int}" @page "/media/movies/page/{PageNumber:int}"
@using LanguageExt.UnsafeValueAccess @using LanguageExt.UnsafeValueAccess
@using Microsoft.AspNetCore.WebUtilities
@using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@ -81,7 +80,7 @@
private MovieCardResultsViewModel _data; private MovieCardResultsViewModel _data;
private string _query; private string _query;
protected override Task OnParametersSetAsync() protected override Task OnParametersSetAsync()
{ {
if (PageNumber == 0) if (PageNumber == 0)
@ -96,12 +95,12 @@
protected override async Task RefreshData() protected override async Task RefreshData()
{ {
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:movie" : $"type:movie AND ({_query})"; string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:movie" : $"type:movie AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexMovies(searchQuery, PageNumber, PageSize)); _data = await Mediator.Send(new QuerySearchIndexMovies(searchQuery, PageNumber, PageSize), CancellationToken);
} }
private void PrevPage() private void PrevPage()
{ {
var uri = $"/media/movies/page/{PageNumber - 1}"; string uri = $"/media/movies/page/{PageNumber - 1}";
if (!string.IsNullOrWhiteSpace(_query)) if (!string.IsNullOrWhiteSpace(_query))
{ {
(string key, string value) = _query.EncodeQuery(); (string key, string value) = _query.EncodeQuery();
@ -112,7 +111,7 @@
private void NextPage() private void NextPage()
{ {
var uri = $"/media/movies/page/{PageNumber + 1}"; string uri = $"/media/movies/page/{PageNumber + 1}";
if (!string.IsNullOrWhiteSpace(_query)) if (!string.IsNullOrWhiteSpace(_query))
{ {
(string key, string value) = _query.EncodeQuery(); (string key, string value) = _query.EncodeQuery();
@ -143,7 +142,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddMovieToCollection(collection.Id, movie.MovieId); var request = new AddMovieToCollection(collection.Id, movie.MovieId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

20
ErsatzTV/Pages/MultiCollectionEditor.razor

@ -3,6 +3,8 @@
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.MediaCollections.Queries @using ErsatzTV.Application.MediaCollections.Queries
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -104,6 +106,8 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -117,17 +121,23 @@
private MudSelect<MediaCollectionViewModel> _collectionSelect; private MudSelect<MediaCollectionViewModel> _collectionSelect;
private MudSelect<SmartCollectionViewModel> _smartCollectionSelect; private MudSelect<SmartCollectionViewModel> _smartCollectionSelect;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_collections = await _mediator.Send(new GetAllCollections()) _collections = await _mediator.Send(new GetAllCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_smartCollections = await _mediator.Send(new GetAllSmartCollections()) _smartCollections = await _mediator.Send(new GetAllSmartCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
if (IsEdit) if (IsEdit)
{ {
Option<MultiCollectionViewModel> maybeCollection = await _mediator.Send(new GetMultiCollectionById(Id)); Option<MultiCollectionViewModel> maybeCollection = await _mediator.Send(new GetMultiCollectionById(Id), _cts.Token);
maybeCollection.IfSome(collection => maybeCollection.IfSome(collection =>
{ {
_model.Id = collection.Id; _model.Id = collection.Id;
@ -170,8 +180,8 @@
if (_editContext.Validate()) if (_editContext.Validate())
{ {
Seq<BaseError> errorMessage = IsEdit ? Seq<BaseError> errorMessage = IsEdit ?
(await _mediator.Send(new UpdateMultiCollection(Id, _model.Name, GetUpdateItems()))).LeftToSeq() : (await _mediator.Send(new UpdateMultiCollection(Id, _model.Name, GetUpdateItems()), _cts.Token)).LeftToSeq() :
(await _mediator.Send(new CreateMultiCollection(_model.Name, GetCreateItems()))).LeftToSeq(); (await _mediator.Send(new CreateMultiCollection(_model.Name, GetCreateItems()), _cts.Token)).LeftToSeq();
errorMessage.HeadOrNone().Match( errorMessage.HeadOrNone().Match(
error => error =>

6
ErsatzTV/Pages/MultiSelectBase.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.MediaCards; using ErsatzTV.Application.MediaCards;
using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaCollections;
@ -137,7 +138,7 @@ namespace ErsatzTV.Pages
otherVideoIds, otherVideoIds,
songIds); songIds);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
@ -173,7 +174,8 @@ namespace ErsatzTV.Pages
new RemoveItemsFromCollection(collectionId) new RemoveItemsFromCollection(collectionId)
{ {
MediaItemIds = itemIds MediaItemIds = itemIds
}); },
CancellationToken);
await RefreshData(); await RefreshData();
ClearSelection(); ClearSelection();

4
ErsatzTV/Pages/MusicVideoList.razor

@ -97,7 +97,7 @@
protected override async Task RefreshData() protected override async Task RefreshData()
{ {
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:music_video" : $"type:music_video AND ({_query})"; string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:music_video" : $"type:music_video AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexMusicVideos(searchQuery, PageNumber, PageSize)); _data = await Mediator.Send(new QuerySearchIndexMusicVideos(searchQuery, PageNumber, PageSize), CancellationToken);
} }
private void PrevPage() private void PrevPage()
@ -144,7 +144,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId); var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

4
ErsatzTV/Pages/OtherVideoList.razor

@ -96,7 +96,7 @@
protected override async Task RefreshData() protected override async Task RefreshData()
{ {
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:other_video" : $"type:other_video AND ({_query})"; string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:other_video" : $"type:other_video AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexOtherVideos(searchQuery, PageNumber, PageSize)); _data = await Mediator.Send(new QuerySearchIndexOtherVideos(searchQuery, PageNumber, PageSize), CancellationToken);
} }
private void PrevPage() private void PrevPage()
@ -143,7 +143,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddOtherVideoToCollection(collection.Id, otherVideo.OtherVideoId); var request = new AddOtherVideoToCollection(collection.Id, otherVideo.OtherVideoId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

15
ErsatzTV/Pages/PlayoutEditor.razor

@ -3,6 +3,8 @@
@using ErsatzTV.Application.Channels.Queries @using ErsatzTV.Application.Channels.Queries
@using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.ProgramSchedules.Queries @using ErsatzTV.Application.ProgramSchedules.Queries
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<PlayoutEditor> _logger @inject ILogger<PlayoutEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -48,6 +50,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private readonly PlayoutEditViewModel _model = new(); private readonly PlayoutEditViewModel _model = new();
private List<ChannelViewModel> _channels; private List<ChannelViewModel> _channels;
@ -55,12 +58,18 @@
private EditContext _editContext; private EditContext _editContext;
private ValidationMessageStore _messageStore; private ValidationMessageStore _messageStore;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_channels = await _mediator.Send(new GetAllChannels()) _channels = await _mediator.Send(new GetAllChannels(), _cts.Token)
.Map(list => list.OrderBy(vm => decimal.Parse(vm.Number)).ToList()); .Map(list => list.OrderBy(vm => decimal.Parse(vm.Number)).ToList());
_programSchedules = await _mediator.Send(new GetAllProgramSchedules()) _programSchedules = await _mediator.Send(new GetAllProgramSchedules(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name).ToList()); .Map(list => list.OrderBy(vm => vm.Name).ToList());
} }
@ -75,7 +84,7 @@
_messageStore.Clear(); _messageStore.Clear();
if (_editContext.Validate()) if (_editContext.Validate())
{ {
Seq<BaseError> errorMessage = (await _mediator.Send(_model.ToCreate())).LeftToSeq(); Seq<BaseError> errorMessage = (await _mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
errorMessage.HeadOrNone().Match( errorMessage.HeadOrNone().Match(
error => error =>

30
ErsatzTV/Pages/Playouts.razor

@ -4,6 +4,8 @@
@using ErsatzTV.Application.Playouts.Queries @using ErsatzTV.Application.Playouts.Queries
@using ErsatzTV.Application.Configuration.Commands @using ErsatzTV.Application.Configuration.Commands
@using ErsatzTV.Application.Configuration.Queries @using ErsatzTV.Application.Configuration.Queries
@using System.Threading
@implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IMediator _mediator @inject IMediator _mediator
@ -99,6 +101,8 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private MudTable<PlayoutNameViewModel> _table; private MudTable<PlayoutNameViewModel> _table;
private MudTable<PlayoutItemViewModel> _detailTable; private MudTable<PlayoutItemViewModel> _detailTable;
private int _rowsPerPage; private int _rowsPerPage;
@ -118,14 +122,20 @@
} }
} }
} }
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsPageSize)) _rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_detailRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize)) _detailRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_showFiller = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller)) _showFiller = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller), _cts.Token)
.Map(maybeShow => maybeShow.Match(ce => bool.TryParse(ce.Value, out bool show) && show, () => false)); .Map(maybeShow => maybeShow.Match(ce => bool.TryParse(ce.Value, out bool show) && show, () => false));
} }
@ -152,7 +162,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _mediator.Send(new DeletePlayout(playout.PlayoutId)); await _mediator.Send(new DeletePlayout(playout.PlayoutId), _cts.Token);
await _table.ReloadServerData(); await _table.ReloadServerData();
if (_selectedPlayoutId == playout.PlayoutId) if (_selectedPlayoutId == playout.PlayoutId)
{ {
@ -163,7 +173,7 @@
private async Task RebuildPlayout(PlayoutNameViewModel playout) private async Task RebuildPlayout(PlayoutNameViewModel playout)
{ {
await _mediator.Send(new BuildPlayout(playout.PlayoutId, true)); await _mediator.Send(new BuildPlayout(playout.PlayoutId, true), _cts.Token);
await _table.ReloadServerData(); await _table.ReloadServerData();
if (_selectedPlayoutId == playout.PlayoutId) if (_selectedPlayoutId == playout.PlayoutId)
{ {
@ -188,9 +198,9 @@
private async Task<TableData<PlayoutNameViewModel>> ServerReload(TableState state) private async Task<TableData<PlayoutNameViewModel>> ServerReload(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsPageSize, state.PageSize.ToString()), _cts.Token);
List<PlayoutNameViewModel> playouts = await _mediator.Send(new GetAllPlayouts()); List<PlayoutNameViewModel> playouts = await _mediator.Send(new GetAllPlayouts(), _cts.Token);
IOrderedEnumerable<PlayoutNameViewModel> sorted = playouts.OrderBy(p => decimal.Parse(p.ChannelNumber)); IOrderedEnumerable<PlayoutNameViewModel> sorted = playouts.OrderBy(p => decimal.Parse(p.ChannelNumber));
// TODO: properly page this data // TODO: properly page this data
@ -203,13 +213,13 @@
private async Task<TableData<PlayoutItemViewModel>> DetailServerReload(TableState state) private async Task<TableData<PlayoutItemViewModel>> DetailServerReload(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize, state.PageSize.ToString()), _cts.Token);
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller, _showFiller.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller, _showFiller.ToString()), _cts.Token);
if (_selectedPlayoutId.HasValue) if (_selectedPlayoutId.HasValue)
{ {
PagedPlayoutItemsViewModel data = PagedPlayoutItemsViewModel data =
await _mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, _showFiller, state.Page, state.PageSize)); await _mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, _showFiller, state.Page, state.PageSize), _cts.Token);
return new TableData<PlayoutItemViewModel> return new TableData<PlayoutItemViewModel>
{ {
TotalItems = data.TotalCount, TotalItems = data.TotalCount,

19
ErsatzTV/Pages/PlexLibrariesEditor.razor

@ -4,6 +4,8 @@
@using ErsatzTV.Application.Plex.Queries @using ErsatzTV.Application.Plex.Queries
@using ErsatzTV.Application.MediaSources @using ErsatzTV.Application.MediaSources
@using ErsatzTV.Application.Plex @using ErsatzTV.Application.Plex
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject ChannelWriter<IPlexBackgroundServiceRequest> _channel @inject ChannelWriter<IPlexBackgroundServiceRequest> _channel
@ -16,6 +18,7 @@
SynchronizeLibraryByIdIfNeeded="SynchronizeLibraryByIdIfNeeded"/> SynchronizeLibraryByIdIfNeeded="SynchronizeLibraryByIdIfNeeded"/>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -24,14 +27,20 @@
new UpdatePlexLibraryPreferences( new UpdatePlexLibraryPreferences(
libraries.Map(l => new PlexLibraryPreference(l.Id, l.ShouldSyncItems)).ToList()); libraries.Map(l => new PlexLibraryPreference(l.Id, l.ShouldSyncItems)).ToList());
private Task<List<RemoteMediaSourceLibraryEditViewModel>> GetLibrariesBySourceId(int mediaSourceId) => private async Task<List<RemoteMediaSourceLibraryEditViewModel>> GetLibrariesBySourceId(int mediaSourceId) =>
_mediator.Send(new GetPlexLibrariesBySourceId(Id)) await _mediator.Send(new GetPlexLibrariesBySourceId(Id), _cts.Token)
.Map(list => list.Map(ProjectToEditViewModel).OrderBy(x => x.MediaKind).ThenBy(x => x.Name).ToList()); .Map(list => list.Map(ProjectToEditViewModel).OrderBy(x => x.MediaKind).ThenBy(x => x.Name).ToList());
private Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int mediaSourceId) => private async Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int mediaSourceId) =>
_mediator.Send(new GetPlexMediaSourceById(Id)) await _mediator.Send(new GetPlexMediaSourceById(Id), _cts.Token)
.MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address)); .MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address));
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private RemoteMediaSourceLibraryEditViewModel ProjectToEditViewModel(PlexLibraryViewModel library) => new() private RemoteMediaSourceLibraryEditViewModel ProjectToEditViewModel(PlexLibraryViewModel library) => new()
{ {
Id = library.Id, Id = library.Id,
@ -42,7 +51,7 @@
private async Task<Unit> SynchronizeLibraryByIdIfNeeded(int libraryId) private async Task<Unit> SynchronizeLibraryByIdIfNeeded(int libraryId)
{ {
await _channel.WriteAsync(new SynchronizePlexLibraryByIdIfNeeded(libraryId)); await _channel.WriteAsync(new SynchronizePlexLibraryByIdIfNeeded(libraryId), _cts.Token);
return Unit.Default; return Unit.Default;
} }

1
ErsatzTV/Pages/PlexMediaSources.razor

@ -3,6 +3,7 @@
@using ErsatzTV.Application.Plex @using ErsatzTV.Application.Plex
@using ErsatzTV.Application.Plex.Commands @using ErsatzTV.Application.Plex.Commands
@using ErsatzTV.Application.Plex.Queries @using ErsatzTV.Application.Plex.Queries
@using System.Threading
@implements IDisposable @implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IMediator _mediator @inject IMediator _mediator

17
ErsatzTV/Pages/PlexPathReplacementsEditor.razor

@ -4,6 +4,8 @@
@using ErsatzTV.Application.Plex.Commands @using ErsatzTV.Application.Plex.Commands
@using ErsatzTV.Application.Plex.Queries @using ErsatzTV.Application.Plex.Queries
@using Unit = LanguageExt.Unit @using Unit = LanguageExt.Unit
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<ScheduleItemsEditor> _logger @inject ILogger<ScheduleItemsEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -17,16 +19,23 @@
GetPathReplacementsBySourceId="GetPathReplacementsBySourceId"/> GetPathReplacementsBySourceId="GetPathReplacementsBySourceId"/>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id) => private async Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id) =>
_mediator.Send(new GetPlexMediaSourceById(Id)) await _mediator.Send(new GetPlexMediaSourceById(Id), _cts.Token)
.MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address)); .MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address));
private Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId) => private async Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId) =>
_mediator.Send(new GetPlexPathReplacementsBySourceId(Id)) await _mediator.Send(new GetPlexPathReplacementsBySourceId(Id), _cts.Token)
.Map(list => list.Map(ProjectToEditViewModel).ToList()); .Map(list => list.Map(ProjectToEditViewModel).ToList());
private RemoteMediaSourcePathReplacementEditViewModel ProjectToEditViewModel(PlexPathReplacementViewModel item) => private RemoteMediaSourcePathReplacementEditViewModel ProjectToEditViewModel(PlexPathReplacementViewModel item) =>

17
ErsatzTV/Pages/ScheduleEditor.razor

@ -2,6 +2,8 @@
@page "/schedules/add" @page "/schedules/add"
@using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.ProgramSchedules.Queries @using ErsatzTV.Application.ProgramSchedules.Queries
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<ScheduleEditor> _logger @inject ILogger<ScheduleEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -50,6 +52,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -57,12 +60,18 @@
private readonly ProgramScheduleEditViewModel _model = new(); private readonly ProgramScheduleEditViewModel _model = new();
private EditContext _editContext; private EditContext _editContext;
private ValidationMessageStore _messageStore; private ValidationMessageStore _messageStore;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if (IsEdit) if (IsEdit)
{ {
Option<ProgramScheduleViewModel> maybeProgramSchedule = await _mediator.Send(new GetProgramScheduleById(Id)); Option<ProgramScheduleViewModel> maybeProgramSchedule = await _mediator.Send(new GetProgramScheduleById(Id), _cts.Token);
maybeProgramSchedule.Match( maybeProgramSchedule.Match(
viewModel => viewModel =>
{ {
@ -93,9 +102,9 @@
_messageStore.Clear(); _messageStore.Clear();
if (_editContext.Validate()) if (_editContext.Validate())
{ {
Either<BaseError, EntityIdResult> result = IsEdit ? Either<BaseError, EntityIdResult> result = IsEdit
await _mediator.Send(_model.ToUpdate()).MapT(r => r as EntityIdResult) : ? await _mediator.Send(_model.ToUpdate(), _cts.Token).MapT(r => r as EntityIdResult)
await _mediator.Send(_model.ToCreate()).MapT(r => r as EntityIdResult); : await _mediator.Send(_model.ToCreate(), _cts.Token).MapT(r => r as EntityIdResult);
result.Match( result.Match(
programSchedule => programSchedule =>

29
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -10,6 +10,8 @@
@using ErsatzTV.Application.Filler @using ErsatzTV.Application.Filler
@using ErsatzTV.Application.Filler.Queries @using ErsatzTV.Application.Filler.Queries
@using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Core.Domain.Filler
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<ScheduleItemsEditor> _logger @inject ILogger<ScheduleItemsEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -289,6 +291,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -303,37 +306,43 @@
private List<FillerPresetViewModel> _fillerPresets; private List<FillerPresetViewModel> _fillerPresets;
private ProgramScheduleItemEditViewModel _selectedItem; private ProgramScheduleItemEditViewModel _selectedItem;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override Task OnParametersSetAsync() => LoadScheduleItems(); protected override Task OnParametersSetAsync() => LoadScheduleItems();
private async Task LoadScheduleItems() private async Task LoadScheduleItems()
{ {
// TODO: fix performance // TODO: fix performance
_mediaCollections = await _mediator.Send(new GetAllCollections()) _mediaCollections = await _mediator.Send(new GetAllCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_multiCollections = await _mediator.Send(new GetAllMultiCollections()) _multiCollections = await _mediator.Send(new GetAllMultiCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_smartCollections = await _mediator.Send(new GetAllSmartCollections()) _smartCollections = await _mediator.Send(new GetAllSmartCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_televisionShows = await _mediator.Send(new GetAllTelevisionShows()) _televisionShows = await _mediator.Send(new GetAllTelevisionShows(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_televisionSeasons = await _mediator.Send(new GetAllTelevisionSeasons()) _televisionSeasons = await _mediator.Send(new GetAllTelevisionSeasons(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_artists = await _mediator.Send(new GetAllArtists()) _artists = await _mediator.Send(new GetAllArtists(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_fillerPresets = await _mediator.Send(new GetAllFillerPresets()) _fillerPresets = await _mediator.Send(new GetAllFillerPresets(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
string name = string.Empty; string name = string.Empty;
var shuffleScheduleItems = false; var shuffleScheduleItems = false;
Option<ProgramScheduleViewModel> maybeSchedule = await _mediator.Send(new GetProgramScheduleById(Id)); Option<ProgramScheduleViewModel> maybeSchedule = await _mediator.Send(new GetProgramScheduleById(Id), _cts.Token);
foreach (ProgramScheduleViewModel schedule in maybeSchedule) foreach (ProgramScheduleViewModel schedule in maybeSchedule)
{ {
name = schedule.Name; name = schedule.Name;
shuffleScheduleItems = schedule.ShuffleScheduleItems; shuffleScheduleItems = schedule.ShuffleScheduleItems;
} }
Option<IEnumerable<ProgramScheduleItemViewModel>> maybeResults = await _mediator.Send(new GetProgramScheduleItems(Id)); Option<IEnumerable<ProgramScheduleItemViewModel>> maybeResults = await _mediator.Send(new GetProgramScheduleItems(Id), _cts.Token);
foreach (IEnumerable<ProgramScheduleItemViewModel> items in maybeResults) foreach (IEnumerable<ProgramScheduleItemViewModel> items in maybeResults)
{ {
_schedule = new ProgramScheduleItemsEditViewModel _schedule = new ProgramScheduleItemsEditViewModel
@ -450,7 +459,7 @@
item.TailFiller?.Id, item.TailFiller?.Id,
item.FallbackFiller?.Id)).ToList(); item.FallbackFiller?.Id)).ToList();
Seq<BaseError> errorMessages = await _mediator.Send(new ReplaceProgramScheduleItems(Id, items)).Map(e => e.LeftToSeq()); Seq<BaseError> errorMessages = await _mediator.Send(new ReplaceProgramScheduleItems(Id, items), _cts.Token).Map(e => e.LeftToSeq());
errorMessages.HeadOrNone().Match( errorMessages.HeadOrNone().Match(
error => error =>

24
ErsatzTV/Pages/Schedules.razor

@ -5,6 +5,8 @@
@using ErsatzTV.Application.Configuration.Queries @using ErsatzTV.Application.Configuration.Queries
@using ErsatzTV.Application.Configuration.Commands @using ErsatzTV.Application.Configuration.Commands
@using NaturalSort.Extension @using NaturalSort.Extension
@using System.Threading
@implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IMediator _mediator @inject IMediator _mediator
@ -90,17 +92,25 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private MudTable<ProgramScheduleViewModel> _table; private MudTable<ProgramScheduleViewModel> _table;
private MudTable<ProgramScheduleItemViewModel> _detailTable; private MudTable<ProgramScheduleItemViewModel> _detailTable;
private int _rowsPerPage; private int _rowsPerPage;
private int _detailRowsPerPage; private int _detailRowsPerPage;
private ProgramScheduleViewModel _selectedSchedule; private ProgramScheduleViewModel _selectedSchedule;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.SchedulesPageSize)) _rowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.SchedulesPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_detailRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.SchedulesDetailPageSize)) _detailRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.SchedulesDetailPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
} }
@ -119,7 +129,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _mediator.Send(new DeleteProgramSchedule(programSchedule.Id)); await _mediator.Send(new DeleteProgramSchedule(programSchedule.Id), _cts.Token);
await _table.ReloadServerData(); await _table.ReloadServerData();
if (_selectedSchedule == programSchedule) if (_selectedSchedule == programSchedule)
{ {
@ -130,9 +140,9 @@
private async Task<TableData<ProgramScheduleViewModel>> ServerReload(TableState state) private async Task<TableData<ProgramScheduleViewModel>> ServerReload(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SchedulesPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SchedulesPageSize, state.PageSize.ToString()), _cts.Token);
List<ProgramScheduleViewModel> schedules = await _mediator.Send(new GetAllProgramSchedules()); List<ProgramScheduleViewModel> schedules = await _mediator.Send(new GetAllProgramSchedules(), _cts.Token);
IOrderedEnumerable<ProgramScheduleViewModel> sorted = schedules.OrderBy(s => s.Name, new NaturalSortComparer(StringComparison.CurrentCultureIgnoreCase)); IOrderedEnumerable<ProgramScheduleViewModel> sorted = schedules.OrderBy(s => s.Name, new NaturalSortComparer(StringComparison.CurrentCultureIgnoreCase));
// TODO: properly page this data // TODO: properly page this data
@ -145,9 +155,9 @@
private async Task<TableData<ProgramScheduleItemViewModel>> DetailServerReload(TableState state) private async Task<TableData<ProgramScheduleItemViewModel>> DetailServerReload(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SchedulesDetailPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SchedulesDetailPageSize, state.PageSize.ToString()), _cts.Token);
List<ProgramScheduleItemViewModel> scheduleItems = await _mediator.Send(new GetProgramScheduleItems(_selectedSchedule.Id)); List<ProgramScheduleItemViewModel> scheduleItems = await _mediator.Send(new GetProgramScheduleItems(_selectedSchedule.Id), _cts.Token);
IOrderedEnumerable<ProgramScheduleItemViewModel> sorted = scheduleItems.OrderBy(s => s.Index); IOrderedEnumerable<ProgramScheduleItemViewModel> sorted = scheduleItems.OrderBy(s => s.Index);
// TODO: properly page this data // TODO: properly page this data

32
ErsatzTV/Pages/Search.razor

@ -333,14 +333,14 @@
_query = _navigationManager.Uri.GetSearchQuery(); _query = _navigationManager.Uri.GetSearchQuery();
if (!string.IsNullOrWhiteSpace(_query)) if (!string.IsNullOrWhiteSpace(_query))
{ {
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50)); _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)); _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)); _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)); _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)); _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)); _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)); _songs = await Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50), CancellationToken);
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50)); _artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), CancellationToken);
} }
} }
@ -374,7 +374,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddMovieToCollection(collection.Id, movie.MovieId); var request = new AddMovieToCollection(collection.Id, movie.MovieId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
@ -395,7 +395,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddShowToCollection(collection.Id, show.TelevisionShowId); var request = new AddShowToCollection(collection.Id, show.TelevisionShowId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
@ -416,7 +416,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId); var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
@ -437,7 +437,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddArtistToCollection(collection.Id, artist.ArtistId); var request = new AddArtistToCollection(collection.Id, artist.ArtistId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
@ -458,7 +458,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId); var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
@ -479,7 +479,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddOtherVideoToCollection(collection.Id, otherVideo.OtherVideoId); var request = new AddOtherVideoToCollection(collection.Id, otherVideo.OtherVideoId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
@ -581,7 +581,7 @@
private async Task AddAllToCollection(MouseEventArgs _) private async Task AddAllToCollection(MouseEventArgs _)
{ {
SearchResultAllItemsViewModel results = await Mediator.Send(new QuerySearchIndexAllItems(_query)); SearchResultAllItemsViewModel results = await Mediator.Send(new QuerySearchIndexAllItems(_query), CancellationToken);
await AddItemsToCollection( await AddItemsToCollection(
results.MovieIds, results.MovieIds,
results.ShowIds, results.ShowIds,
@ -606,7 +606,7 @@
collection.Id, collection.Id,
_query); _query);
Either<BaseError, Unit> updateResult = await Mediator.Send(request); Either<BaseError, Unit> updateResult = await Mediator.Send(request, CancellationToken);
updateResult.Match( updateResult.Match(
Left: error => Left: error =>
{ {

34
ErsatzTV/Pages/Settings.razor

@ -16,6 +16,8 @@
@using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Core.Domain.Filler
@using ErsatzTV.Core.FFmpeg @using ErsatzTV.Core.FFmpeg
@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@inject ILogger<Settings> _logger @inject ILogger<Settings> _logger
@ -177,6 +179,8 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private bool _success; private bool _success;
private bool _hdhrSuccess; private bool _hdhrSuccess;
private bool _scannerSuccess; private bool _scannerSuccess;
@ -189,22 +193,28 @@
private int _tunerCount; private int _tunerCount;
private int _libraryRefreshInterval; private int _libraryRefreshInterval;
private int _playoutDaysToBuild; private int _playoutDaysToBuild;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
await LoadFFmpegProfilesAsync(); await LoadFFmpegProfilesAsync();
_ffmpegSettings = await _mediator.Send(new GetFFmpegSettings()); _ffmpegSettings = await _mediator.Send(new GetFFmpegSettings(), _cts.Token);
_success = File.Exists(_ffmpegSettings.FFmpegPath) && File.Exists(_ffmpegSettings.FFprobePath); _success = File.Exists(_ffmpegSettings.FFmpegPath) && File.Exists(_ffmpegSettings.FFprobePath);
_availableCultures = await _mediator.Send(new GetAllLanguageCodes()); _availableCultures = await _mediator.Send(new GetAllLanguageCodes(), _cts.Token);
_watermarks = await _mediator.Send(new GetAllWatermarks()); _watermarks = await _mediator.Send(new GetAllWatermarks(), _cts.Token);
_fillerPresets = await _mediator.Send(new GetAllFillerPresets()) _fillerPresets = await _mediator.Send(new GetAllFillerPresets(), _cts.Token)
.Map(list => list.Filter(fp => fp.FillerKind == FillerKind.Fallback).ToList()); .Map(list => list.Filter(fp => fp.FillerKind == FillerKind.Fallback).ToList());
_tunerCount = await _mediator.Send(new GetHDHRTunerCount()); _tunerCount = await _mediator.Send(new GetHDHRTunerCount(), _cts.Token);
_hdhrSuccess = string.IsNullOrWhiteSpace(ValidateTunerCount(_tunerCount)); _hdhrSuccess = string.IsNullOrWhiteSpace(ValidateTunerCount(_tunerCount));
_libraryRefreshInterval = await _mediator.Send(new GetLibraryRefreshInterval()); _libraryRefreshInterval = await _mediator.Send(new GetLibraryRefreshInterval(), _cts.Token);
_scannerSuccess = _libraryRefreshInterval > 0; _scannerSuccess = _libraryRefreshInterval > 0;
_playoutDaysToBuild = await _mediator.Send(new GetPlayoutDaysToBuild()); _playoutDaysToBuild = await _mediator.Send(new GetPlayoutDaysToBuild(), _cts.Token);
_playoutSuccess = _playoutDaysToBuild > 0; _playoutSuccess = _playoutDaysToBuild > 0;
} }
@ -223,11 +233,11 @@
private static string ValidateInitialSegmentCount(int count) => count < 1 ? "HLS Segmenter initial segment count must be greater than or equal to 1" : null; private static string ValidateInitialSegmentCount(int count) => count < 1 ? "HLS Segmenter initial segment count must be greater than or equal to 1" : null;
private async Task LoadFFmpegProfilesAsync() => private async Task LoadFFmpegProfilesAsync() =>
_ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles()); _ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles(), _cts.Token);
private async Task SaveFFmpegSettings() private async Task SaveFFmpegSettings()
{ {
Either<BaseError, Unit> result = await _mediator.Send(new UpdateFFmpegSettings(_ffmpegSettings)); Either<BaseError, Unit> result = await _mediator.Send(new UpdateFFmpegSettings(_ffmpegSettings), _cts.Token);
result.Match( result.Match(
Left: error => Left: error =>
{ {
@ -239,7 +249,7 @@
private async Task SaveHDHRSettings() private async Task SaveHDHRSettings()
{ {
Either<BaseError, Unit> result = await _mediator.Send(new UpdateHDHRTunerCount(_tunerCount)); Either<BaseError, Unit> result = await _mediator.Send(new UpdateHDHRTunerCount(_tunerCount), _cts.Token);
result.Match( result.Match(
Left: error => Left: error =>
{ {
@ -251,7 +261,7 @@
private async Task SaveScannerSettings() private async Task SaveScannerSettings()
{ {
Either<BaseError, Unit> result = await _mediator.Send(new UpdateLibraryRefreshInterval(_libraryRefreshInterval)); Either<BaseError, Unit> result = await _mediator.Send(new UpdateLibraryRefreshInterval(_libraryRefreshInterval), _cts.Token);
result.Match( result.Match(
Left: error => Left: error =>
{ {
@ -263,7 +273,7 @@
private async Task SavePlayoutSettings() private async Task SavePlayoutSettings()
{ {
Either<BaseError, Unit> result = await _mediator.Send(new UpdatePlayoutDaysToBuild(_playoutDaysToBuild)); Either<BaseError, Unit> result = await _mediator.Send(new UpdatePlayoutDaysToBuild(_playoutDaysToBuild), _cts.Token);
result.Match( result.Match(
Left: error => Left: error =>
{ {

4
ErsatzTV/Pages/SongList.razor

@ -96,7 +96,7 @@
protected override async Task RefreshData() protected override async Task RefreshData()
{ {
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:song" : $"type:song AND ({_query})"; string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:song" : $"type:song AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexSongs(searchQuery, PageNumber, PageSize)); _data = await Mediator.Send(new QuerySearchIndexSongs(searchQuery, PageNumber, PageSize), CancellationToken);
} }
private void PrevPage() private void PrevPage()
@ -143,7 +143,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddSongToCollection(collection.Id, song.SongId); var request = new AddSongToCollection(collection.Id, song.SongId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

19
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -9,6 +9,8 @@
@using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.ProgramSchedules.Commands @using ErsatzTV.Application.ProgramSchedules.Commands
@using Unit = LanguageExt.Unit @using Unit = LanguageExt.Unit
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject ILogger<TelevisionEpisodeList> _logger @inject ILogger<TelevisionEpisodeList> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -156,6 +158,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int SeasonId { get; set; } public int SeasonId { get; set; }
@ -166,6 +169,12 @@
private readonly int _pageNumber = 1; private readonly int _pageNumber = 1;
private TelevisionEpisodeCardResultsViewModel _data; private TelevisionEpisodeCardResultsViewModel _data;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
@ -179,10 +188,10 @@
private async Task RefreshData() private async Task RefreshData()
{ {
await _mediator.Send(new GetTelevisionSeasonById(SeasonId)) await _mediator.Send(new GetTelevisionSeasonById(SeasonId), _cts.Token)
.IfSomeAsync(vm => _season = vm); .IfSomeAsync(vm => _season = vm);
_data = await _mediator.Send(new GetTelevisionEpisodeCards(SeasonId, _pageNumber, _pageSize)); _data = await _mediator.Send(new GetTelevisionEpisodeCards(SeasonId, _pageNumber, _pageSize), _cts.Token);
} }
private async Task AddToCollection() private async Task AddToCollection()
@ -194,7 +203,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
await _mediator.Send(new AddSeasonToCollection(collection.Id, SeasonId)); await _mediator.Send(new AddSeasonToCollection(collection.Id, SeasonId), _cts.Token);
_navigationManager.NavigateTo($"/media/collections/{collection.Id}"); _navigationManager.NavigateTo($"/media/collections/{collection.Id}");
} }
} }
@ -209,7 +218,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule) if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
{ {
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null)); await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null), _cts.Token);
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); _navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
} }
} }
@ -224,7 +233,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId); var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId);
Either<BaseError, Unit> addResult = await _mediator.Send(request); Either<BaseError, Unit> addResult = await _mediator.Send(request, _cts.Token);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

19
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -10,6 +10,8 @@
@using ErsatzTV.Application.ProgramSchedules.Commands @using ErsatzTV.Application.ProgramSchedules.Commands
@using ErsatzTV.Extensions @using ErsatzTV.Extensions
@using Unit = LanguageExt.Unit @using Unit = LanguageExt.Unit
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject ILogger<TelevisionSeasonList> _logger @inject ILogger<TelevisionSeasonList> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -168,6 +170,7 @@
} }
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int ShowId { get; set; } public int ShowId { get; set; }
@ -183,12 +186,18 @@
private readonly int _pageNumber = 1; private readonly int _pageNumber = 1;
private TelevisionSeasonCardResultsViewModel _data; private TelevisionSeasonCardResultsViewModel _data;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override Task OnParametersSetAsync() => RefreshData(); protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData() private async Task RefreshData()
{ {
await _mediator.Send(new GetTelevisionShowById(ShowId)) await _mediator.Send(new GetTelevisionShowById(ShowId), _cts.Token)
.IfSomeAsync(vm => .IfSomeAsync(vm =>
{ {
_show = vm; _show = vm;
@ -199,7 +208,7 @@
_sortedTags = _show.Tags.OrderBy(t => t).ToList(); _sortedTags = _show.Tags.OrderBy(t => t).ToList();
}); });
_data = await _mediator.Send(new GetTelevisionSeasonCards(ShowId, _pageNumber, _pageSize)); _data = await _mediator.Send(new GetTelevisionSeasonCards(ShowId, _pageNumber, _pageSize), _cts.Token);
} }
private async Task AddToCollection() private async Task AddToCollection()
@ -211,7 +220,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
await _mediator.Send(new AddShowToCollection(collection.Id, ShowId)); await _mediator.Send(new AddShowToCollection(collection.Id, ShowId), _cts.Token);
_navigationManager.NavigateTo($"/media/collections/{collection.Id}"); _navigationManager.NavigateTo($"/media/collections/{collection.Id}");
} }
} }
@ -225,7 +234,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule) if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
{ {
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null)); await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null), _cts.Token);
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); _navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
} }
} }
@ -242,7 +251,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddSeasonToCollection(collection.Id, season.TelevisionSeasonId); var request = new AddSeasonToCollection(collection.Id, season.TelevisionSeasonId);
Either<BaseError, Unit> addResult = await _mediator.Send(request); Either<BaseError, Unit> addResult = await _mediator.Send(request, _cts.Token);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

4
ErsatzTV/Pages/TelevisionSeasonSearchResults.razor

@ -95,7 +95,7 @@
protected override async Task RefreshData() protected override async Task RefreshData()
{ {
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:season" : $"type:season AND ({_query})"; string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:season" : $"type:season AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexSeasons(searchQuery, PageNumber, PageSize)); _data = await Mediator.Send(new QuerySearchIndexSeasons(searchQuery, PageNumber, PageSize), CancellationToken);
} }
private void PrevPage() private void PrevPage()
@ -142,7 +142,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddSeasonToCollection(collection.Id, season.TelevisionSeasonId); var request = new AddSeasonToCollection(collection.Id, season.TelevisionSeasonId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

4
ErsatzTV/Pages/TelevisionShowList.razor

@ -95,7 +95,7 @@
protected override async Task RefreshData() protected override async Task RefreshData()
{ {
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:show" : $"type:show AND ({_query})"; string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:show" : $"type:show AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexShows(searchQuery, PageNumber, PageSize)); _data = await Mediator.Send(new QuerySearchIndexShows(searchQuery, PageNumber, PageSize), CancellationToken);
} }
private void PrevPage() private void PrevPage()
@ -142,7 +142,7 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddShowToCollection(collection.Id, show.TelevisionShowId); var request = new AddShowToCollection(collection.Id, show.TelevisionShowId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {

22
ErsatzTV/Pages/TraktLists.razor

@ -4,6 +4,8 @@
@using ErsatzTV.Application.Configuration.Queries @using ErsatzTV.Application.Configuration.Queries
@using ErsatzTV.Application.Configuration.Commands @using ErsatzTV.Application.Configuration.Commands
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using System.Threading
@implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IMediator _mediator @inject IMediator _mediator
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -75,9 +77,17 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private MudTable<TraktListViewModel> _traktListsTable; private MudTable<TraktListViewModel> _traktListsTable;
private int _traktListsRowsPerPage; private int _traktListsRowsPerPage;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override void OnInitialized() protected override void OnInitialized()
{ {
@ -86,7 +96,7 @@
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_traktListsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.TraktListsPageSize)) _traktListsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.TraktListsPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
} }
@ -104,7 +114,7 @@
{ {
if (_locker.LockTrakt()) if (_locker.LockTrakt())
{ {
await _workerChannel.WriteAsync(new MatchTraktListItems(traktList.Id)); await _workerChannel.WriteAsync(new MatchTraktListItems(traktList.Id), _cts.Token);
} }
} }
@ -119,16 +129,16 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _workerChannel.WriteAsync(new DeleteTraktList(traktList.Id)); await _workerChannel.WriteAsync(new DeleteTraktList(traktList.Id), _cts.Token);
} }
} }
} }
private async Task<TableData<TraktListViewModel>> ServerReloadTraktLists(TableState state) private async Task<TableData<TraktListViewModel>> ServerReloadTraktLists(TableState state)
{ {
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.TraktListsPageSize, state.PageSize.ToString())); await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.TraktListsPageSize, state.PageSize.ToString()), _cts.Token);
PagedTraktListsViewModel data = await _mediator.Send(new GetPagedTraktLists(state.Page, state.PageSize)); PagedTraktListsViewModel data = await _mediator.Send(new GetPagedTraktLists(state.Page, state.PageSize), _cts.Token);
return new TableData<TraktListViewModel> { TotalItems = data.TotalCount, Items = data.Page }; return new TableData<TraktListViewModel> { TotalItems = data.TotalCount, Items = data.Page };
} }
@ -141,7 +151,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is string url) if (!result.Cancelled && result.Data is string url)
{ {
await _workerChannel.WriteAsync(new AddTraktList(url)); await _workerChannel.WriteAsync(new AddTraktList(url), _cts.Token);
} }
} }
} }

20
ErsatzTV/Pages/Trash.razor

@ -327,14 +327,14 @@
_query = "state:FileNotFound"; _query = "state:FileNotFound";
if (!string.IsNullOrWhiteSpace(_query)) if (!string.IsNullOrWhiteSpace(_query))
{ {
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50)); _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)); _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)); _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)); _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)); _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)); _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)); _songs = await Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50), CancellationToken);
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50)); _artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), CancellationToken);
} }
} }
@ -486,7 +486,7 @@
.Append(songIds) .Append(songIds)
.ToList()); .ToList());
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
await addResult.Match( await addResult.Match(
Left: error => Left: error =>
{ {
@ -556,7 +556,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await Mediator.Send(request); await Mediator.Send(request, CancellationToken);
await RefreshData(); await RefreshData();
} }
} }

22
ErsatzTV/Pages/WatermarkEditor.razor

@ -4,6 +4,8 @@
@using ErsatzTV.Application.Watermarks @using ErsatzTV.Application.Watermarks
@using ErsatzTV.Application.Watermarks.Queries @using ErsatzTV.Application.Watermarks.Queries
@using ErsatzTV.FFmpeg.State @using ErsatzTV.FFmpeg.State
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<WatermarkEditor> _logger @inject ILogger<WatermarkEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -146,7 +148,8 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -154,11 +157,17 @@
private EditContext _editContext; private EditContext _editContext;
private ValidationMessageStore _messageStore; private ValidationMessageStore _messageStore;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if (IsEdit) if (IsEdit)
{ {
Option<WatermarkViewModel> watermark = await _mediator.Send(new GetWatermarkById(Id)); Option<WatermarkViewModel> watermark = await _mediator.Send(new GetWatermarkById(Id), _cts.Token);
watermark.Match( watermark.Match(
watermarkViewModel => _model = new WatermarkEditViewModel(watermarkViewModel), watermarkViewModel => _model = new WatermarkEditViewModel(watermarkViewModel),
() => _navigationManager.NavigateTo("404")); () => _navigationManager.NavigateTo("404"));
@ -193,8 +202,8 @@
if (_editContext.Validate()) if (_editContext.Validate())
{ {
Seq<BaseError> errorMessage = IsEdit ? Seq<BaseError> errorMessage = IsEdit ?
(await _mediator.Send(_model.ToUpdate())).LeftToSeq() : (await _mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() :
(await _mediator.Send(_model.ToCreate())).LeftToSeq(); (await _mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
errorMessage.HeadOrNone().Match( errorMessage.HeadOrNone().Match(
error => error =>
@ -210,8 +219,9 @@
{ {
try try
{ {
Either<BaseError, string> maybeCacheFileName = await _mediator Either<BaseError, string> maybeCacheFileName = await _mediator.Send(
.Send(new SaveArtworkToDisk(e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024), ArtworkKind.Watermark)); new SaveArtworkToDisk(e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024), ArtworkKind.Watermark),
_cts.Token);
maybeCacheFileName.Match( maybeCacheFileName.Match(
relativeFileName => relativeFileName =>
{ {

14
ErsatzTV/Pages/Watermarks.razor

@ -2,6 +2,8 @@
@using ErsatzTV.Application.Watermarks @using ErsatzTV.Application.Watermarks
@using ErsatzTV.Application.Watermarks.Commands @using ErsatzTV.Application.Watermarks.Commands
@using ErsatzTV.Application.Watermarks.Queries @using ErsatzTV.Application.Watermarks.Queries
@using System.Threading
@implements IDisposable
@inject IDialogService _dialog @inject IDialogService _dialog
@inject IMediator _mediator @inject IMediator _mediator
@inject ILogger<Watermarks> _logger @inject ILogger<Watermarks> _logger
@ -72,12 +74,20 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
private List<WatermarkViewModel> _watermarks; private List<WatermarkViewModel> _watermarks;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadWatermarksAsync(); protected override async Task OnParametersSetAsync() => await LoadWatermarksAsync();
private async Task LoadWatermarksAsync() => private async Task LoadWatermarksAsync() =>
_watermarks = await _mediator.Send(new GetAllWatermarks()); _watermarks = await _mediator.Send(new GetAllWatermarks(), _cts.Token);
private async Task DeleteWatermarkAsync(WatermarkViewModel watermark) private async Task DeleteWatermarkAsync(WatermarkViewModel watermark)
{ {
@ -88,7 +98,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled) if (!result.Cancelled)
{ {
await _mediator.Send(new DeleteWatermark(watermark.Id)); await _mediator.Send(new DeleteWatermark(watermark.Id), _cts.Token);
await LoadWatermarksAsync(); await LoadWatermarksAsync();
} }
} }

13
ErsatzTV/Shared/AddToCollectionDialog.razor

@ -2,6 +2,8 @@
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.MediaCollections.Queries @using ErsatzTV.Application.MediaCollections.Queries
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject IMemoryCache _memoryCache @inject IMemoryCache _memoryCache
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -39,6 +41,7 @@
@code { @code {
private readonly CancellationTokenSource _cts = new();
[CascadingParameter] [CascadingParameter]
MudDialogInstance MudDialog { get; set; } MudDialogInstance MudDialog { get; set; }
@ -65,13 +68,19 @@
private record DummyModel; private record DummyModel;
private readonly DummyModel _dummyModel = new(); private readonly DummyModel _dummyModel = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private bool CanSubmit() => private bool CanSubmit() =>
_selectedCollection != null && (_selectedCollection != _newCollection || !string.IsNullOrWhiteSpace(_newCollectionName)); _selectedCollection != null && (_selectedCollection != _newCollection || !string.IsNullOrWhiteSpace(_newCollectionName));
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_collections = await _mediator.Send(new GetAllCollections()) _collections = await _mediator.Send(new GetAllCollections(), _cts.Token)
.Map(list => new[] { _newCollection }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList()); .Map(list => new[] { _newCollection }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList());
if (_memoryCache.TryGetValue("AddToCollectionDialog.SelectedCollectionId", out int id)) if (_memoryCache.TryGetValue("AddToCollectionDialog.SelectedCollectionId", out int id))
@ -96,7 +105,7 @@
if (_selectedCollection == _newCollection) if (_selectedCollection == _newCollection)
{ {
Either<BaseError, MediaCollectionViewModel> maybeResult = Either<BaseError, MediaCollectionViewModel> maybeResult =
await _mediator.Send(new CreateCollection(_newCollectionName)); await _mediator.Send(new CreateCollection(_newCollectionName), _cts.Token);
maybeResult.Match( maybeResult.Match(
collection => collection =>

11
ErsatzTV/Shared/AddToScheduleDialog.razor

@ -1,6 +1,8 @@
@using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.ProgramSchedules.Queries @using ErsatzTV.Application.ProgramSchedules.Queries
@using NaturalSort.Extension @using NaturalSort.Extension
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
<div @onkeydown="@OnKeyDown"> <div @onkeydown="@OnKeyDown">
@ -29,6 +31,7 @@
</div> </div>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[CascadingParameter] [CascadingParameter]
MudDialogInstance MudDialog { get; set; } MudDialogInstance MudDialog { get; set; }
@ -48,9 +51,15 @@
private List<ProgramScheduleViewModel> _schedules; private List<ProgramScheduleViewModel> _schedules;
private ProgramScheduleViewModel _selectedSchedule; private ProgramScheduleViewModel _selectedSchedule;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() => protected override async Task OnParametersSetAsync() =>
_schedules = await _mediator.Send(new GetAllProgramSchedules()) _schedules = await _mediator.Send(new GetAllProgramSchedules(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, new NaturalSortComparer(StringComparison.CurrentCultureIgnoreCase)).ToList()); .Map(list => list.OrderBy(vm => vm.Name, new NaturalSortComparer(StringComparison.CurrentCultureIgnoreCase)).ToList());
private string FormatText() => $"Select the schedule to add the {EntityType} {EntityName}"; private string FormatText() => $"Select the schedule to add the {EntityType} {EntityName}";

11
ErsatzTV/Shared/CopyFFmpegProfileDialog.razor

@ -1,5 +1,7 @@
@using ErsatzTV.Application.FFmpegProfiles @using ErsatzTV.Application.FFmpegProfiles
@using ErsatzTV.Application.FFmpegProfiles.Commands @using ErsatzTV.Application.FFmpegProfiles.Commands
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@inject ILogger<CopyFFmpegProfileDialog> _logger @inject ILogger<CopyFFmpegProfileDialog> _logger
@ -27,6 +29,7 @@
</MudDialog> </MudDialog>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[CascadingParameter] [CascadingParameter]
MudDialogInstance MudDialog { get; set; } MudDialogInstance MudDialog { get; set; }
@ -40,6 +43,12 @@
private string _newFFmpegProfileName; private string _newFFmpegProfileName;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newFFmpegProfileName); private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newFFmpegProfileName);
private async Task Submit() private async Task Submit()
@ -50,7 +59,7 @@
} }
Either<BaseError, FFmpegProfileViewModel> maybeResult = Either<BaseError, FFmpegProfileViewModel> maybeResult =
await _mediator.Send(new CopyFFmpegProfile(FFmpegProfileId, _newFFmpegProfileName)); await _mediator.Send(new CopyFFmpegProfile(FFmpegProfileId, _newFFmpegProfileName), _cts.Token);
maybeResult.Match( maybeResult.Match(
ffmpegProfile => { MudDialog.Close(DialogResult.Ok(ffmpegProfile)); }, ffmpegProfile => { MudDialog.Close(DialogResult.Ok(ffmpegProfile)); },

11
ErsatzTV/Shared/CopyWatermarkDialog.razor

@ -1,5 +1,7 @@
@using ErsatzTV.Application.Watermarks @using ErsatzTV.Application.Watermarks
@using ErsatzTV.Application.Watermarks.Commands @using ErsatzTV.Application.Watermarks.Commands
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@inject ILogger<CopyWatermarkDialog> _logger @inject ILogger<CopyWatermarkDialog> _logger
@ -27,6 +29,7 @@
</MudDialog> </MudDialog>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[CascadingParameter] [CascadingParameter]
MudDialogInstance MudDialog { get; set; } MudDialogInstance MudDialog { get; set; }
@ -39,6 +42,12 @@
private readonly DummyModel _dummyModel = new(); private readonly DummyModel _dummyModel = new();
private string _newWatermarkName; private string _newWatermarkName;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newWatermarkName); private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newWatermarkName);
@ -50,7 +59,7 @@
} }
Either<BaseError, WatermarkViewModel> maybeResult = Either<BaseError, WatermarkViewModel> maybeResult =
await _mediator.Send(new CopyWatermark(WatermarkId, _newWatermarkName)); await _mediator.Send(new CopyWatermark(WatermarkId, _newWatermarkName), _cts.Token);
maybeResult.Match( maybeResult.Match(
watermark => { MudDialog.Close(DialogResult.Ok(watermark)); }, watermark => { MudDialog.Close(DialogResult.Ok(watermark)); },

13
ErsatzTV/Shared/MoveLocalLibraryPathDialog.razor

@ -2,6 +2,8 @@
@using ErsatzTV.Application.Libraries @using ErsatzTV.Application.Libraries
@using ErsatzTV.Application.Libraries.Commands @using ErsatzTV.Application.Libraries.Commands
@using ErsatzTV.Application.Libraries.Queries @using ErsatzTV.Application.Libraries.Queries
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject IMemoryCache _memoryCache @inject IMemoryCache _memoryCache
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -38,6 +40,7 @@
@code { @code {
private readonly CancellationTokenSource _cts = new();
[CascadingParameter] [CascadingParameter]
MudDialogInstance MudDialog { get; set; } MudDialogInstance MudDialog { get; set; }
@ -58,6 +61,12 @@
private record DummyModel; private record DummyModel;
private readonly DummyModel _dummyModel = new(); private readonly DummyModel _dummyModel = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private bool CanSubmit() => private bool CanSubmit() =>
_selectedLibrary != null && (_selectedLibrary != _newLibrary || !string.IsNullOrWhiteSpace(_newLibraryName)); _selectedLibrary != null && (_selectedLibrary != _newLibrary || !string.IsNullOrWhiteSpace(_newLibraryName));
@ -66,7 +75,7 @@
{ {
_newLibrary = new LocalLibraryViewModel(-1, "(New Library)", MediaKind); _newLibrary = new LocalLibraryViewModel(-1, "(New Library)", MediaKind);
_libraries = await _mediator.Send(new GetAllLocalLibraries()) _libraries = await _mediator.Send(new GetAllLocalLibraries(), _cts.Token)
.Map(list => list.Filter(ll => ll.MediaKind == MediaKind && ll.Id != SourceLibraryId)) .Map(list => list.Filter(ll => ll.MediaKind == MediaKind && ll.Id != SourceLibraryId))
.Map(list => new[] { _newLibrary }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList()); .Map(list => new[] { _newLibrary }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList());
@ -90,7 +99,7 @@
if (_selectedLibrary == _newLibrary) if (_selectedLibrary == _newLibrary)
{ {
Either<BaseError, LocalLibraryViewModel> maybeResult = Either<BaseError, LocalLibraryViewModel> maybeResult =
await _mediator.Send(new CreateLocalLibrary(_newLibraryName, MediaKind, new List<string>())); await _mediator.Send(new CreateLocalLibrary(_newLibraryName, MediaKind, new List<string>()), _cts.Token);
maybeResult.Match( maybeResult.Match(
collection => collection =>

11
ErsatzTV/Shared/RemoteMediaSourceLibrariesEditor.razor

@ -1,5 +1,7 @@
@using ErsatzTV.Application.MediaSources @using ErsatzTV.Application.MediaSources
@using Unit = LanguageExt.Unit @using Unit = LanguageExt.Unit
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<RemoteMediaSourceLibrariesEditor> _logger @inject ILogger<RemoteMediaSourceLibrariesEditor> _logger
@ -43,6 +45,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -64,6 +67,12 @@
private RemoteMediaSourceViewModel _source; private RemoteMediaSourceViewModel _source;
private List<RemoteMediaSourceLibraryEditViewModel> _libraries; private List<RemoteMediaSourceLibraryEditViewModel> _libraries;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override Task OnParametersSetAsync() => LoadData(); protected override Task OnParametersSetAsync() => LoadData();
@ -86,7 +95,7 @@
private async Task SaveChanges() private async Task SaveChanges()
{ {
IRequest<Either<BaseError, Unit>> request = GetUpdateLibraryRequest(_libraries); IRequest<Either<BaseError, Unit>> request = GetUpdateLibraryRequest(_libraries);
Seq<BaseError> errorMessages = await _mediator.Send(request).Map(e => e.LeftToSeq()); Seq<BaseError> errorMessages = await _mediator.Send(request, _cts.Token).Map(e => e.LeftToSeq());
await errorMessages.HeadOrNone().Match( await errorMessages.HeadOrNone().Match(
error => error =>

11
ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor

@ -1,5 +1,7 @@
@using ErsatzTV.Application.MediaSources @using ErsatzTV.Application.MediaSources
@using Unit = LanguageExt.Unit @using Unit = LanguageExt.Unit
@using System.Threading
@implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ILogger<RemoteMediaSourcePathReplacementsEditor> _logger @inject ILogger<RemoteMediaSourcePathReplacementsEditor> _logger
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -69,6 +71,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
@ -89,6 +92,12 @@
private List<RemoteMediaSourcePathReplacementEditViewModel> _pathReplacements; private List<RemoteMediaSourcePathReplacementEditViewModel> _pathReplacements;
private RemoteMediaSourcePathReplacementEditViewModel _selectedItem; private RemoteMediaSourcePathReplacementEditViewModel _selectedItem;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override Task OnParametersSetAsync() => LoadData(); protected override Task OnParametersSetAsync() => LoadData();
@ -123,7 +132,7 @@
private async Task SaveChanges() private async Task SaveChanges()
{ {
Seq<BaseError> errorMessages = await _mediator.Send(GetUpdatePathReplacementsRequest(_pathReplacements)) Seq<BaseError> errorMessages = await _mediator.Send(GetUpdatePathReplacementsRequest(_pathReplacements), _cts.Token)
.Map(e => e.LeftToSeq()); .Map(e => e.LeftToSeq());
errorMessages.HeadOrNone().Match( errorMessages.HeadOrNone().Match(

13
ErsatzTV/Shared/RemoteMediaSources.razor

@ -2,6 +2,8 @@
@typeparam TSecrets @typeparam TSecrets
@using Unit = LanguageExt.Unit @using Unit = LanguageExt.Unit
@using ErsatzTV.Core.Interfaces.MediaSources @using ErsatzTV.Core.Interfaces.MediaSources
@using System.Threading
@implements IDisposable
@typeparam TMediaSource @typeparam TMediaSource
@inject IMediator _mediator @inject IMediator _mediator
@inject IDialogService _dialog @inject IDialogService _dialog
@ -79,6 +81,7 @@
</MudContainer> </MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[Parameter] [Parameter]
public string Name { get; set; } public string Name { get; set; }
@ -98,6 +101,12 @@
private List<TViewModel> _mediaSources = new(); private List<TViewModel> _mediaSources = new();
private bool _isAuthorized; private bool _isAuthorized;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadMediaSources(); protected override async Task OnParametersSetAsync() => await LoadMediaSources();
@ -105,7 +114,7 @@
{ {
_isAuthorized = await SecretStore.ReadSecrets() _isAuthorized = await SecretStore.ReadSecrets()
.Map(secrets => !string.IsNullOrWhiteSpace(secrets.Address) && !string.IsNullOrWhiteSpace(secrets.ApiKey)); .Map(secrets => !string.IsNullOrWhiteSpace(secrets.Address) && !string.IsNullOrWhiteSpace(secrets.ApiKey));
_mediaSources = await _mediator.Send(GetAllMediaSourcesCommand); _mediaSources = await _mediator.Send(GetAllMediaSourcesCommand, _cts.Token);
} }
private async Task Disconnect() private async Task Disconnect()
@ -118,7 +127,7 @@
{ {
if (_locker.LockRemoteMediaSource<TMediaSource>()) if (_locker.LockRemoteMediaSource<TMediaSource>())
{ {
await _mediator.Send(DisconnectCommand); await _mediator.Send(DisconnectCommand, _cts.Token);
await LoadMediaSources(); await LoadMediaSources();
} }
} }

13
ErsatzTV/Shared/SaveAsSmartCollectionDialog.razor

@ -2,6 +2,8 @@
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.MediaCollections.Queries @using ErsatzTV.Application.MediaCollections.Queries
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject IMemoryCache _memoryCache @inject IMemoryCache _memoryCache
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@ -38,6 +40,7 @@
@code { @code {
private readonly CancellationTokenSource _cts = new();
[CascadingParameter] [CascadingParameter]
MudDialogInstance MudDialog { get; set; } MudDialogInstance MudDialog { get; set; }
@ -52,13 +55,19 @@
private record DummyModel; private record DummyModel;
private readonly DummyModel _dummyModel = new(); private readonly DummyModel _dummyModel = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private bool CanSubmit() => private bool CanSubmit() =>
_selectedCollection != null && (_selectedCollection != _newCollection || !string.IsNullOrWhiteSpace(_newCollectionName)); _selectedCollection != null && (_selectedCollection != _newCollection || !string.IsNullOrWhiteSpace(_newCollectionName));
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_collections = await _mediator.Send(new GetAllSmartCollections()) _collections = await _mediator.Send(new GetAllSmartCollections(), _cts.Token)
.Map(list => new[] { _newCollection }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList()); .Map(list => new[] { _newCollection }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList());
if (_memoryCache.TryGetValue("SaveAsSmartCollectionDialog.SelectedCollectionId", out int id)) if (_memoryCache.TryGetValue("SaveAsSmartCollectionDialog.SelectedCollectionId", out int id))
@ -81,7 +90,7 @@
if (_selectedCollection == _newCollection) if (_selectedCollection == _newCollection)
{ {
Either<BaseError, SmartCollectionViewModel> maybeResult = Either<BaseError, SmartCollectionViewModel> maybeResult =
await _mediator.Send(new CreateSmartCollection(string.Empty, _newCollectionName)); await _mediator.Send(new CreateSmartCollection(string.Empty, _newCollectionName), _cts.Token);
maybeResult.Match( maybeResult.Match(
collection => collection =>

11
ErsatzTV/Shared/SchedulePlayoutRebuild.razor

@ -1,6 +1,8 @@
@using System.Globalization @using System.Globalization
@using ErsatzTV.Application.Playouts @using ErsatzTV.Application.Playouts
@using ErsatzTV.Application.Playouts.Commands @using ErsatzTV.Application.Playouts.Commands
@using System.Threading
@implements IDisposable
@inject IMediator _mediator @inject IMediator _mediator
@inject ISnackbar _snackbar @inject ISnackbar _snackbar
@inject ILogger<SchedulePlayoutRebuild> _logger @inject ILogger<SchedulePlayoutRebuild> _logger
@ -33,6 +35,7 @@
</MudDialog> </MudDialog>
@code { @code {
private readonly CancellationTokenSource _cts = new();
[CascadingParameter] [CascadingParameter]
MudDialogInstance MudDialog { get; set; } MudDialogInstance MudDialog { get; set; }
@ -56,6 +59,12 @@
private readonly DummyModel _dummyModel = new(); private readonly DummyModel _dummyModel = new();
private Option<TimeSpan> _rebuildTime; private Option<TimeSpan> _rebuildTime;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
@ -65,7 +74,7 @@
private async Task Submit() private async Task Submit()
{ {
Either<BaseError, PlayoutNameViewModel> maybeResult = Either<BaseError, PlayoutNameViewModel> maybeResult =
await _mediator.Send(new UpdatePlayout(PlayoutId, _rebuildTime)); await _mediator.Send(new UpdatePlayout(PlayoutId, _rebuildTime), _cts.Token);
maybeResult.Match( maybeResult.Match(
playout => { MudDialog.Close(DialogResult.Ok(playout)); }, playout => { MudDialog.Close(DialogResult.Ok(playout)); },

Loading…
Cancel
Save