Browse Source

fix mysql playout builds (#2352)

* more cancellation tokens and fixes

* so much cancellation token

* fix mysql playout builds
pull/2353/head
Jason Dove 4 months ago committed by GitHub
parent
commit
9462156148
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs
  2. 10
      ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs
  3. 2
      ErsatzTV.Application/Channels/Queries/GetChannelLineupHandler.cs
  4. 2
      ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs
  5. 10
      ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs
  6. 60
      ErsatzTV.Application/Watermarks/Commands/CopyWatermarkHandler.cs
  7. 2
      ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs
  8. 5
      ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs
  9. 5
      ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs
  10. 37
      ErsatzTV.Core/Interfaces/Repositories/IMediaServerTelevisionRepository.cs
  11. 16
      ErsatzTV.Core/Interfaces/Repositories/IPlexTelevisionRepository.cs
  12. 8
      ErsatzTV.Core/Interfaces/Repositories/IRemoteStreamRepository.cs
  13. 4
      ErsatzTV.Infrastructure/Data/DbInitializer.cs
  14. 6
      ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs
  15. 357
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  16. 331
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  17. 372
      ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs
  18. 68
      ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs
  19. 7
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs
  20. 13
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs
  21. 2
      ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexNetworksHandler.cs
  22. 14
      ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexShowByIdHandler.cs
  23. 4
      ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs
  24. 15
      ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs
  25. 42
      ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  26. 13
      ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs
  27. 4
      ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs
  28. 38
      ErsatzTV/Pages/Artist.razor
  29. 72
      ErsatzTV/Pages/BlockEditor.razor
  30. 52
      ErsatzTV/Pages/BlockPlayoutEditor.razor
  31. 32
      ErsatzTV/Pages/Blocks.razor
  32. 207
      ErsatzTV/Pages/ChannelEditor.razor
  33. 6
      ErsatzTV/Pages/Channels.razor
  34. 34
      ErsatzTV/Pages/CollectionEditor.razor
  35. 42
      ErsatzTV/Pages/Collections.razor
  36. 144
      ErsatzTV/Pages/DecoEditor.razor
  37. 38
      ErsatzTV/Pages/DecoTemplateEditor.razor
  38. 32
      ErsatzTV/Pages/DecoTemplates.razor
  39. 32
      ErsatzTV/Pages/Decos.razor
  40. 17
      ErsatzTV/Pages/EmbyMediaSourceEditor.razor
  41. 16
      ErsatzTV/Pages/EmbyMediaSources.razor
  42. 17
      ErsatzTV/Pages/EmbyPathReplacementsEditor.razor
  43. 25
      ErsatzTV/Pages/FFmpeg.razor
  44. 138
      ErsatzTV/Pages/FillerPresetEditor.razor
  45. 29
      ErsatzTV/Pages/FillerPresets.razor
  46. 26
      ErsatzTV/Pages/ImageBrowser.razor
  47. 17
      ErsatzTV/Pages/Index.razor
  48. 17
      ErsatzTV/Pages/JellyfinMediaSourceEditor.razor
  49. 16
      ErsatzTV/Pages/JellyfinMediaSources.razor
  50. 17
      ErsatzTV/Pages/JellyfinPathReplacementsEditor.razor
  51. 57
      ErsatzTV/Pages/Libraries.razor
  52. 58
      ErsatzTV/Pages/LocalLibraryEditor.razor
  53. 34
      ErsatzTV/Pages/Logs.razor
  54. 57
      ErsatzTV/Pages/Movie.razor
  55. 72
      ErsatzTV/Pages/MultiCollectionEditor.razor
  56. 50
      ErsatzTV/Pages/PlaybackTroubleshooting.razor
  57. 60
      ErsatzTV/Pages/PlaylistEditor.razor
  58. 30
      ErsatzTV/Pages/Playlists.razor
  59. 36
      ErsatzTV/Pages/PlayoutAlternateSchedulesEditor.razor
  60. 30
      ErsatzTV/Pages/PlayoutEditor.razor
  61. 40
      ErsatzTV/Pages/PlayoutTemplatesEditor.razor
  62. 40
      ErsatzTV/Pages/Playouts.razor
  63. 17
      ErsatzTV/Pages/PlexPathReplacementsEditor.razor
  64. 50
      ErsatzTV/Pages/ScheduleEditor.razor
  65. 80
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  66. 34
      ErsatzTV/Pages/Schedules.razor
  67. 42
      ErsatzTV/Pages/ScriptedPlayoutEditor.razor
  68. 42
      ErsatzTV/Pages/SequentialPlayoutEditor.razor
  69. 45
      ErsatzTV/Pages/Settings/FFmpegSettings.razor
  70. 24
      ErsatzTV/Pages/Settings/HDHRSettings.razor
  71. 23
      ErsatzTV/Pages/Settings/LoggingSettings.razor
  72. 22
      ErsatzTV/Pages/Settings/PlayoutSettings.razor
  73. 22
      ErsatzTV/Pages/Settings/ScannerSettings.razor
  74. 23
      ErsatzTV/Pages/Settings/XMLTVSettings.razor
  75. 28
      ErsatzTV/Pages/SmartCollectionEditor.razor
  76. 26
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  77. 46
      ErsatzTV/Pages/TelevisionSeasonList.razor
  78. 15
      ErsatzTV/Pages/TelevisionShowList.razor
  79. 38
      ErsatzTV/Pages/TemplateEditor.razor
  80. 32
      ErsatzTV/Pages/Templates.razor
  81. 38
      ErsatzTV/Pages/TraktListEditor.razor
  82. 31
      ErsatzTV/Pages/TraktLists.razor
  83. 10
      ErsatzTV/Pages/Trash.razor
  84. 17
      ErsatzTV/Pages/Troubleshooting.razor
  85. 72
      ErsatzTV/Pages/WatermarkEditor.razor
  86. 26
      ErsatzTV/Pages/Watermarks.razor
  87. 6
      ErsatzTV/Pages/YamlValidator.razor
  88. 32
      ErsatzTV/Shared/AddToCollectionDialog.razor
  89. 32
      ErsatzTV/Shared/AddToPlaylistDialog.razor
  90. 26
      ErsatzTV/Shared/AddToScheduleDialog.razor
  91. 8
      ErsatzTV/Shared/CopyFFmpegProfileDialog.razor
  92. 8
      ErsatzTV/Shared/CopyScheduleDialog.razor
  93. 8
      ErsatzTV/Shared/CopyWatermarkDialog.razor
  94. 16
      ErsatzTV/Shared/EditExternalJsonFileDialog.razor
  95. 16
      ErsatzTV/Shared/EditImageFolderDurationDialog.razor
  96. 46
      ErsatzTV/Shared/MainLayout.razor
  97. 36
      ErsatzTV/Shared/MoveLocalLibraryPathDialog.razor
  98. 32
      ErsatzTV/Shared/RemoteMediaSourceEditor.razor
  99. 4
      ErsatzTV/Shared/RemoteMediaSourceLibrariesEditor.razor
  100. 50
      ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor
  101. Some files were not shown because too many files have changed in this diff Show More

2
ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs

@ -15,7 +15,7 @@ public class GetAllChannelsForApiHandler : IRequestHandler<GetAllChannelsForApi, @@ -15,7 +15,7 @@ public class GetAllChannelsForApiHandler : IRequestHandler<GetAllChannelsForApi,
GetAllChannelsForApi request,
CancellationToken cancellationToken)
{
IEnumerable<Channel> channels = Optional(await _channelRepository.GetAll()).Flatten();
IEnumerable<Channel> channels = Optional(await _channelRepository.GetAll(cancellationToken)).Flatten();
return channels.Map(ProjectToResponseModel).ToList();
}
}

10
ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs

@ -3,12 +3,10 @@ using static ErsatzTV.Application.Channels.Mapper; @@ -3,12 +3,10 @@ using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels;
public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<ChannelViewModel>>
public class GetAllChannelsHandler(IChannelRepository channelRepository)
: IRequestHandler<GetAllChannels, List<ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
await channelRepository.GetAll(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
}

2
ErsatzTV.Application/Channels/Queries/GetChannelLineupHandler.cs

@ -10,7 +10,7 @@ public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<Li @@ -10,7 +10,7 @@ public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<Li
public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
_channelRepository.GetAll(cancellationToken)
.Map(channels => channels.Where(c => c.IsEnabled)
.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
}

2
ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs

@ -12,7 +12,7 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha @@ -12,7 +12,7 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
_channelRepository = channelRepository;
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
_channelRepository.GetAll(cancellationToken)
.Map(channels => EnsureMode(channels, request.Mode))
.Map(channels => new ChannelPlaylist(
request.Scheme,

10
ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs

@ -4,18 +4,14 @@ using static ErsatzTV.Application.FFmpegProfiles.Mapper; @@ -4,18 +4,14 @@ using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles;
public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>>
public class GetAllFFmpegProfilesHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllFFmpegProfilesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<FFmpegProfileViewModel>> Handle(
GetAllFFmpegProfiles request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.FFmpegProfiles
.Include(p => p.Resolution)
.ToListAsync(cancellationToken)

60
ErsatzTV.Application/Watermarks/Commands/CopyWatermarkHandler.cs

@ -2,55 +2,63 @@ @@ -2,55 +2,63 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using static ErsatzTV.Application.Watermarks.Mapper;
namespace ErsatzTV.Application.Watermarks;
public class CopyWatermarkHandler :
IRequestHandler<CopyWatermark, Either<BaseError, WatermarkViewModel>>
public class CopyWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
: IRequestHandler<CopyWatermark, Either<BaseError, WatermarkViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ISearchTargets _searchTargets;
public CopyWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
public async Task<Either<BaseError, WatermarkViewModel>> Handle(
CopyWatermark request,
CancellationToken cancellationToken)
{
_dbContextFactory = dbContextFactory;
_searchTargets = searchTargets;
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, CopyWatermarkParameters> validation = await Validate(dbContext, request, cancellationToken);
return await validation.Apply(c => PerformCopy(dbContext, c, cancellationToken));
}
public Task<Either<BaseError, WatermarkViewModel>> Handle(
CopyWatermark request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PerformCopy)
.Bind(v => v.ToEitherAsync());
private async Task<WatermarkViewModel> PerformCopy(CopyWatermark request)
private async Task<WatermarkViewModel> PerformCopy(
TvContext dbContext,
CopyWatermarkParameters parameters,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
ChannelWatermark channelWatermark = await dbContext.ChannelWatermarks.FindAsync(request.WatermarkId);
PropertyValues values = dbContext.Entry(channelWatermark).CurrentValues.Clone();
PropertyValues values = dbContext.Entry(parameters.Watermark).CurrentValues.Clone();
values["Id"] = 0;
var clone = new ChannelWatermark();
await dbContext.AddAsync(clone);
await dbContext.AddAsync(clone, cancellationToken);
dbContext.Entry(clone).CurrentValues.SetValues(values);
clone.Name = request.Name;
clone.Name = parameters.Name;
await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
_searchTargets.SearchTargetsChanged();
searchTargets.SearchTargetsChanged();
return ProjectToViewModel(clone);
}
private static Task<Validation<BaseError, CopyWatermark>> Validate(CopyWatermark request) =>
ValidateName(request).AsTask().MapT(_ => request);
private static async Task<Validation<BaseError, CopyWatermarkParameters>> Validate(
TvContext dbContext,
CopyWatermark request,
CancellationToken cancellationToken) =>
(ValidateName(request), await WatermarkMustExist(dbContext, request, cancellationToken))
.Apply((name, watermark) => new CopyWatermarkParameters(name, watermark));
private static Validation<BaseError, string> ValidateName(CopyWatermark request) =>
request.NotEmpty(x => x.Name)
.Bind(_ => request.NotLongerThan(50)(x => x.Name));
private static Task<Validation<BaseError, ChannelWatermark>> WatermarkMustExist(
TvContext dbContext,
CopyWatermark copyWatermark,
CancellationToken cancellationToken) =>
dbContext.ChannelWatermarks
.SelectOneAsync(wm => wm.Id, wm => wm.Id == copyWatermark.WatermarkId, cancellationToken)
.Map(o => o.ToValidation<BaseError>($"Watermark {copyWatermark.WatermarkId} does not exist."));
private sealed record CopyWatermarkParameters(string Name, ChannelWatermark Watermark);
}

2
ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs

@ -6,6 +6,6 @@ public interface IChannelRepository @@ -6,6 +6,6 @@ public interface IChannelRepository
{
Task<Option<Channel>> GetChannel(int id);
Task<Option<Channel>> GetByNumber(string number);
Task<List<Channel>> GetAll();
Task<List<Channel>> GetAll(CancellationToken cancellationToken);
Task<Option<ChannelWatermark>> GetWatermarkByName(string name);
}

5
ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs

@ -6,7 +6,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories; @@ -6,7 +6,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories;
public interface IEmbyTelevisionRepository : IMediaServerTelevisionRepository<EmbyLibrary, EmbyShow, EmbySeason,
EmbyEpisode, EmbyItemEtag>
{
Task<Option<EmbyShowTitleItemIdResult>> GetShowTitleItemId(int libraryId, int showId);
Task<Option<EmbyShowTitleItemIdResult>> GetShowTitleItemId(
int libraryId,
int showId,
CancellationToken cancellationToken);
}
public record EmbyShowTitleItemIdResult(string Title, string ItemId);

5
ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs

@ -7,7 +7,10 @@ public interface IJellyfinTelevisionRepository : IMediaServerTelevisionRepositor @@ -7,7 +7,10 @@ public interface IJellyfinTelevisionRepository : IMediaServerTelevisionRepositor
JellyfinSeason,
JellyfinEpisode, JellyfinItemEtag>
{
Task<Option<JellyfinShowTitleItemIdResult>> GetShowTitleItemId(int libraryId, int showId);
Task<Option<JellyfinShowTitleItemIdResult>> GetShowTitleItemId(
int libraryId,
int showId,
CancellationToken cancellationToken);
}
public record JellyfinShowTitleItemIdResult(string Title, string ItemId);

37
ErsatzTV.Core/Interfaces/Repositories/IMediaServerTelevisionRepository.cs

@ -9,9 +9,9 @@ public interface IMediaServerTelevisionRepository<in TLibrary, TShow, TSeason, T @@ -9,9 +9,9 @@ public interface IMediaServerTelevisionRepository<in TLibrary, TShow, TSeason, T
where TEpisode : Episode
where TEtag : MediaServerItemEtag
{
Task<List<TEtag>> GetExistingShows(TLibrary library);
Task<List<TEtag>> GetExistingSeasons(TLibrary library, TShow show);
Task<List<TEtag>> GetExistingEpisodes(TLibrary library, TSeason season);
Task<List<TEtag>> GetExistingShows(TLibrary library, CancellationToken cancellationToken);
Task<List<TEtag>> GetExistingSeasons(TLibrary library, TShow show, CancellationToken cancellationToken);
Task<List<TEtag>> GetExistingEpisodes(TLibrary library, TSeason season, CancellationToken cancellationToken);
Task<Either<BaseError, MediaItemScanResult<TShow>>> GetOrAdd(
TLibrary library,
@ -29,15 +29,24 @@ public interface IMediaServerTelevisionRepository<in TLibrary, TShow, TSeason, T @@ -29,15 +29,24 @@ public interface IMediaServerTelevisionRepository<in TLibrary, TShow, TSeason, T
bool deepScan,
CancellationToken cancellationToken);
Task<Unit> SetEtag(TShow show, string etag);
Task<Unit> SetEtag(TSeason season, string etag);
Task<Unit> SetEtag(TEpisode episode, string etag);
Task<Option<int>> FlagNormal(TLibrary library, TEpisode episode);
Task<Option<int>> FlagNormal(TLibrary library, TSeason season);
Task<Option<int>> FlagNormal(TLibrary library, TShow show);
Task<List<int>> FlagFileNotFoundShows(TLibrary library, List<string> showItemIds);
Task<List<int>> FlagFileNotFoundSeasons(TLibrary library, List<string> seasonItemIds);
Task<List<int>> FlagFileNotFoundEpisodes(TLibrary library, List<string> episodeItemIds);
Task<Option<int>> FlagUnavailable(TLibrary library, TEpisode episode);
Task<Option<int>> FlagRemoteOnly(TLibrary library, TEpisode episode);
Task<Unit> SetEtag(TShow show, string etag, CancellationToken cancellationToken);
Task<Unit> SetEtag(TSeason season, string etag, CancellationToken cancellationToken);
Task<Unit> SetEtag(TEpisode episode, string etag, CancellationToken cancellationToken);
Task<Option<int>> FlagNormal(TLibrary library, TEpisode episode, CancellationToken cancellationToken);
Task<Option<int>> FlagNormal(TLibrary library, TSeason season, CancellationToken cancellationToken);
Task<Option<int>> FlagNormal(TLibrary library, TShow show, CancellationToken cancellationToken);
Task<List<int>> FlagFileNotFoundShows(
TLibrary library,
List<string> showItemIds,
CancellationToken cancellationToken);
Task<List<int>> FlagFileNotFoundSeasons(
TLibrary library,
List<string> seasonItemIds,
CancellationToken cancellationToken);
Task<List<int>> FlagFileNotFoundEpisodes(
TLibrary library,
List<string> episodeItemIds,
CancellationToken cancellationToken);
Task<Option<int>> FlagUnavailable(TLibrary library, TEpisode episode, CancellationToken cancellationToken);
Task<Option<int>> FlagRemoteOnly(TLibrary library, TEpisode episode, CancellationToken cancellationToken);
}

16
ErsatzTV.Core/Interfaces/Repositories/IPlexTelevisionRepository.cs

@ -6,10 +6,18 @@ namespace ErsatzTV.Core.Interfaces.Repositories; @@ -6,10 +6,18 @@ namespace ErsatzTV.Core.Interfaces.Repositories;
public interface IPlexTelevisionRepository : IMediaServerTelevisionRepository<PlexLibrary, PlexShow, PlexSeason,
PlexEpisode, PlexItemEtag>
{
Task<List<int>> RemoveAllTags(PlexLibrary library, PlexTag tag, System.Collections.Generic.HashSet<int> keep);
Task<PlexShowAddTagResult> AddTag(PlexLibrary library, PlexShow show, PlexTag tag);
Task UpdateLastNetworksScan(PlexLibrary library);
Task<Option<PlexShowTitleKeyResult>> GetShowTitleKey(int libraryId, int showId);
Task<List<int>> RemoveAllTags(
PlexLibrary library,
PlexTag tag,
System.Collections.Generic.HashSet<int> keep,
CancellationToken cancellationToken);
Task<PlexShowAddTagResult> AddTag(
PlexLibrary library,
PlexShow show,
PlexTag tag,
CancellationToken cancellationToken);
Task UpdateLastNetworksScan(PlexLibrary library, CancellationToken cancellationToken);
Task<Option<PlexShowTitleKeyResult>> GetShowTitleKey(int libraryId, int showId, CancellationToken cancellationToken);
}
public record PlexShowAddTagResult(Option<int> Existing, Option<int> Added);

8
ErsatzTV.Core/Interfaces/Repositories/IRemoteStreamRepository.cs

@ -11,8 +11,8 @@ public interface IRemoteStreamRepository @@ -11,8 +11,8 @@ public interface IRemoteStreamRepository
string path,
CancellationToken cancellationToken);
Task<IEnumerable<string>> FindRemoteStreamPaths(LibraryPath libraryPath);
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
Task<bool> AddTag(RemoteStreamMetadata metadata, Tag tag);
Task UpdateDefinition(RemoteStream remoteStream);
Task<IEnumerable<string>> FindRemoteStreamPaths(LibraryPath libraryPath, CancellationToken cancellationToken);
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path, CancellationToken cancellationToken);
Task<bool> AddTag(RemoteStreamMetadata metadata, Tag tag, CancellationToken cancellationToken);
Task UpdateDefinition(RemoteStream remoteStream, CancellationToken cancellationToken);
}

4
ErsatzTV.Infrastructure/Data/DbInitializer.cs

@ -13,6 +13,10 @@ public static class DbInitializer @@ -13,6 +13,10 @@ public static class DbInitializer
{
await context.Connection.ExecuteAsync("PRAGMA journal_mode=WAL", cancellationToken);
}
else
{
await context.Connection.ExecuteAsync("SET GLOBAL local_infile = true", cancellationToken);
}
if (!context.LanguageCodes.Any())
{

6
ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs

@ -37,15 +37,15 @@ public class ChannelRepository : IChannelRepository @@ -37,15 +37,15 @@ public class ChannelRepository : IChannelRepository
.Map(Optional);
}
public async Task<List<Channel>> GetAll()
public async Task<List<Channel>> GetAll(CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Channels
.AsNoTracking()
.Include(c => c.FFmpegProfile)
.Include(c => c.Artwork)
.Include(c => c.Playouts)
.ToListAsync();
.ToListAsync(cancellationToken);
}
public async Task<Option<ChannelWatermark>> GetWatermarkByName(string name)

357
ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs

@ -11,59 +11,62 @@ using Microsoft.Extensions.Logging; @@ -11,59 +11,62 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class EmbyTelevisionRepository : IEmbyTelevisionRepository
public class EmbyTelevisionRepository(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<EmbyTelevisionRepository> logger) : IEmbyTelevisionRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<EmbyTelevisionRepository> _logger;
public EmbyTelevisionRepository(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<EmbyTelevisionRepository> logger)
public async Task<List<EmbyItemEtag>> GetExistingShows(EmbyLibrary library, CancellationToken cancellationToken)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<List<EmbyItemEtag>> GetExistingShows(EmbyLibrary library)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<EmbyItemEtag>(
@"SELECT ItemId, Etag, MI.State FROM EmbyShow
new CommandDefinition(
@"SELECT ItemId, Etag, MI.State FROM EmbyShow
INNER JOIN `Show` S on EmbyShow.Id = S.Id
INNER JOIN MediaItem MI on S.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id
WHERE LP.LibraryId = @LibraryId",
new { LibraryId = library.Id })
parameters: new { LibraryId = library.Id },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
}
public async Task<List<EmbyItemEtag>> GetExistingSeasons(EmbyLibrary library, EmbyShow show)
public async Task<List<EmbyItemEtag>> GetExistingSeasons(
EmbyLibrary library,
EmbyShow show,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<EmbyItemEtag>(
@"SELECT EmbySeason.ItemId, EmbySeason.Etag, MI.State FROM EmbySeason
new CommandDefinition(
@"SELECT EmbySeason.ItemId, EmbySeason.Etag, MI.State FROM EmbySeason
INNER JOIN Season S on EmbySeason.Id = S.Id
INNER JOIN MediaItem MI on S.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id
INNER JOIN `Show` S2 on S.ShowId = S2.Id
INNER JOIN EmbyShow JS on S2.Id = JS.Id
WHERE LP.LibraryId = @LibraryId AND JS.ItemId = @ShowItemId",
new { LibraryId = library.Id, ShowItemId = show.ItemId })
parameters: new { LibraryId = library.Id, ShowItemId = show.ItemId },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
}
public async Task<List<EmbyItemEtag>> GetExistingEpisodes(EmbyLibrary library, EmbySeason season)
public async Task<List<EmbyItemEtag>> GetExistingEpisodes(
EmbyLibrary library,
EmbySeason season,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<EmbyItemEtag>(
@"SELECT EmbyEpisode.ItemId, EmbyEpisode.Etag, MI.State FROM EmbyEpisode
new CommandDefinition(
@"SELECT EmbyEpisode.ItemId, EmbyEpisode.Etag, MI.State FROM EmbyEpisode
INNER JOIN Episode E on EmbyEpisode.Id = E.Id
INNER JOIN MediaItem MI on E.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id
INNER JOIN Season S2 on E.SeasonId = S2.Id
INNER JOIN EmbySeason JS on S2.Id = JS.Id
WHERE LP.LibraryId = @LibraryId AND JS.ItemId = @SeasonItemId",
new { LibraryId = library.Id, SeasonItemId = season.ItemId })
parameters: new { LibraryId = library.Id, SeasonItemId = season.ItemId },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
}
@ -72,7 +75,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -72,7 +75,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
EmbyShow item,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<EmbyShow> maybeExisting = await dbContext.EmbyShows
.Include(m => m.LibraryPath)
.ThenInclude(lp => lp.Library)
@ -97,14 +100,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -97,14 +100,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
var result = new MediaItemScanResult<EmbyShow>(embyShow) { IsAdded = false };
if (embyShow.Etag != item.Etag)
{
await UpdateShow(dbContext, embyShow, item);
await UpdateShow(dbContext, embyShow, item, cancellationToken);
result.IsUpdated = true;
}
return result;
}
return await AddShow(dbContext, library, item);
return await AddShow(dbContext, library, item, cancellationToken);
}
public async Task<Either<BaseError, MediaItemScanResult<EmbySeason>>> GetOrAdd(
@ -112,7 +115,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -112,7 +115,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
EmbySeason item,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<EmbySeason> maybeExisting = await dbContext.EmbySeasons
.Include(m => m.LibraryPath)
.Include(m => m.SeasonMetadata)
@ -126,14 +129,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -126,14 +129,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
var result = new MediaItemScanResult<EmbySeason>(embySeason) { IsAdded = false };
if (embySeason.Etag != item.Etag)
{
await UpdateSeason(dbContext, embySeason, item);
await UpdateSeason(dbContext, embySeason, item, cancellationToken);
result.IsUpdated = true;
}
return result;
}
return await AddSeason(dbContext, library, item);
return await AddSeason(dbContext, library, item, cancellationToken);
}
public async Task<Either<BaseError, MediaItemScanResult<EmbyEpisode>>> GetOrAdd(
@ -142,7 +145,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -142,7 +145,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
bool deepScan,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<EmbyEpisode> maybeExisting = await dbContext.EmbyEpisodes
.Include(m => m.LibraryPath)
.ThenInclude(lp => lp.Library)
@ -178,7 +181,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -178,7 +181,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
var result = new MediaItemScanResult<EmbyEpisode>(embyEpisode) { IsAdded = false };
if (embyEpisode.Etag != item.Etag || deepScan)
{
await UpdateEpisode(dbContext, embyEpisode, item);
await UpdateEpisode(dbContext, embyEpisode, item, cancellationToken);
result.IsUpdated = true;
}
@ -188,254 +191,316 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -188,254 +191,316 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
return await AddEpisode(dbContext, library, item, cancellationToken);
}
public async Task<Unit> SetEtag(EmbyShow show, string etag)
public async Task<Unit> SetEtag(EmbyShow show, string etag, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.ExecuteAsync(
"UPDATE EmbyShow SET Etag = @Etag WHERE Id = @Id",
new { Etag = etag, show.Id }).Map(_ => Unit.Default);
new CommandDefinition(
"UPDATE EmbyShow SET Etag = @Etag WHERE Id = @Id",
parameters: new { Etag = etag, show.Id },
cancellationToken: cancellationToken)).Map(_ => Unit.Default);
}
public async Task<Unit> SetEtag(EmbySeason season, string etag)
public async Task<Unit> SetEtag(EmbySeason season, string etag, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.ExecuteAsync(
"UPDATE EmbySeason SET Etag = @Etag WHERE Id = @Id",
new { Etag = etag, season.Id }).Map(_ => Unit.Default);
new CommandDefinition(
"UPDATE EmbySeason SET Etag = @Etag WHERE Id = @Id",
parameters: new { Etag = etag, season.Id },
cancellationToken: cancellationToken)).Map(_ => Unit.Default);
}
public async Task<Unit> SetEtag(EmbyEpisode episode, string etag)
public async Task<Unit> SetEtag(EmbyEpisode episode, string etag, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.ExecuteAsync(
"UPDATE EmbyEpisode SET Etag = @Etag WHERE Id = @Id",
new { Etag = etag, episode.Id }).Map(_ => Unit.Default);
new CommandDefinition(
"UPDATE EmbyEpisode SET Etag = @Etag WHERE Id = @Id",
parameters: new { Etag = etag, episode.Id },
cancellationToken: cancellationToken)).Map(_ => Unit.Default);
}
public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbyEpisode episode)
public async Task<Option<int>> FlagNormal(
EmbyLibrary library,
EmbyEpisode episode,
CancellationToken cancellationToken)
{
if (episode.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
episode.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT EmbyEpisode.Id FROM EmbyEpisode
INNER JOIN MediaItem MI ON MI.Id = EmbyEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbyEpisode.ItemId = @ItemId",
new { LibraryId = library.Id, episode.ItemId });
new CommandDefinition(
@"SELECT EmbyEpisode.Id FROM EmbyEpisode
INNER JOIN MediaItem MI ON MI.Id = EmbyEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbyEpisode.ItemId = @ItemId",
parameters: new { LibraryId = library.Id, episode.ItemId },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbySeason season)
public async Task<Option<int>> FlagNormal(
EmbyLibrary library,
EmbySeason season,
CancellationToken cancellationToken)
{
if (season.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
season.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT EmbySeason.Id FROM EmbySeason
INNER JOIN MediaItem MI ON MI.Id = EmbySeason.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbySeason.ItemId = @ItemId",
new { LibraryId = library.Id, season.ItemId });
new CommandDefinition(
@"SELECT EmbySeason.Id FROM EmbySeason
INNER JOIN MediaItem MI ON MI.Id = EmbySeason.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbySeason.ItemId = @ItemId",
parameters: new { LibraryId = library.Id, season.ItemId },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbyShow show)
public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbyShow show, CancellationToken cancellationToken)
{
if (show.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
show.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT EmbyShow.Id FROM EmbyShow
INNER JOIN MediaItem MI ON MI.Id = EmbyShow.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbyShow.ItemId = @ItemId",
new { LibraryId = library.Id, show.ItemId });
new CommandDefinition(
@"SELECT EmbyShow.Id FROM EmbyShow
INNER JOIN MediaItem MI ON MI.Id = EmbyShow.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbyShow.ItemId = @ItemId",
parameters: new { LibraryId = library.Id, show.ItemId },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<List<int>> FlagFileNotFoundShows(EmbyLibrary library, List<string> showItemIds)
public async Task<List<int>> FlagFileNotFoundShows(
EmbyLibrary library,
List<string> showItemIds,
CancellationToken cancellationToken)
{
if (showItemIds.Count == 0)
{
return [];
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN EmbyShow ON EmbyShow.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE EmbyShow.ItemId IN @ShowItemIds",
new { LibraryId = library.Id, ShowItemIds = showItemIds })
new CommandDefinition(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN EmbyShow ON EmbyShow.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE EmbyShow.ItemId IN @ShowItemIds",
parameters: new { LibraryId = library.Id, ShowItemIds = showItemIds },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids });
new CommandDefinition(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
parameters: new { Ids = ids },
cancellationToken: cancellationToken));
return ids;
}
public async Task<List<int>> FlagFileNotFoundSeasons(EmbyLibrary library, List<string> seasonItemIds)
public async Task<List<int>> FlagFileNotFoundSeasons(
EmbyLibrary library,
List<string> seasonItemIds,
CancellationToken cancellationToken)
{
if (seasonItemIds.Count == 0)
{
return [];
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN EmbySeason ON EmbySeason.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE EmbySeason.ItemId IN @SeasonItemIds",
new { LibraryId = library.Id, SeasonItemIds = seasonItemIds })
new CommandDefinition(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN EmbySeason ON EmbySeason.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE EmbySeason.ItemId IN @SeasonItemIds",
parameters: new { LibraryId = library.Id, SeasonItemIds = seasonItemIds },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids });
new CommandDefinition(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
parameters: new { Ids = ids },
cancellationToken: cancellationToken));
return ids;
}
public async Task<List<int>> FlagFileNotFoundEpisodes(EmbyLibrary library, List<string> episodeItemIds)
public async Task<List<int>> FlagFileNotFoundEpisodes(
EmbyLibrary library,
List<string> episodeItemIds,
CancellationToken cancellationToken)
{
if (episodeItemIds.Count == 0)
{
return [];
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN EmbyEpisode ON EmbyEpisode.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE EmbyEpisode.ItemId IN @EpisodeItemIds",
new { LibraryId = library.Id, EpisodeItemIds = episodeItemIds })
new CommandDefinition(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN EmbyEpisode ON EmbyEpisode.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE EmbyEpisode.ItemId IN @EpisodeItemIds",
parameters: new { LibraryId = library.Id, EpisodeItemIds = episodeItemIds },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids });
new CommandDefinition(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
parameters: new { Ids = ids },
cancellationToken: cancellationToken));
return ids;
}
public async Task<Option<int>> FlagUnavailable(EmbyLibrary library, EmbyEpisode episode)
public async Task<Option<int>> FlagUnavailable(
EmbyLibrary library,
EmbyEpisode episode,
CancellationToken cancellationToken)
{
if (episode.State is MediaItemState.Unavailable)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
episode.State = MediaItemState.Unavailable;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT EmbyEpisode.Id FROM EmbyEpisode
INNER JOIN MediaItem MI ON MI.Id = EmbyEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbyEpisode.ItemId = @ItemId",
new { LibraryId = library.Id, episode.ItemId });
new CommandDefinition(
@"SELECT EmbyEpisode.Id FROM EmbyEpisode
INNER JOIN MediaItem MI ON MI.Id = EmbyEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbyEpisode.ItemId = @ItemId",
parameters: new { LibraryId = library.Id, episode.ItemId },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<int>> FlagRemoteOnly(EmbyLibrary library, EmbyEpisode episode)
public async Task<Option<int>> FlagRemoteOnly(
EmbyLibrary library,
EmbyEpisode episode,
CancellationToken cancellationToken)
{
if (episode.State is MediaItemState.RemoteOnly)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
episode.State = MediaItemState.RemoteOnly;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT EmbyEpisode.Id FROM EmbyEpisode
INNER JOIN MediaItem MI ON MI.Id = EmbyEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbyEpisode.ItemId = @ItemId",
new { LibraryId = library.Id, episode.ItemId });
new CommandDefinition(
@"SELECT EmbyEpisode.Id FROM EmbyEpisode
INNER JOIN MediaItem MI ON MI.Id = EmbyEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbyEpisode.ItemId = @ItemId",
parameters: new { LibraryId = library.Id, episode.ItemId },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<EmbyShowTitleItemIdResult>> GetShowTitleItemId(int libraryId, int showId)
public async Task<Option<EmbyShowTitleItemIdResult>> GetShowTitleItemId(
int libraryId,
int showId,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<EmbyShow> maybeShow = await dbContext.EmbyShows
.Where(s => s.Id == showId)
.Where(s => s.LibraryPath.LibraryId == libraryId)
.Include(s => s.ShowMetadata)
.FirstOrDefaultAsync()
.FirstOrDefaultAsync(cancellationToken)
.Map(Optional);
foreach (EmbyShow show in maybeShow)
@ -448,7 +513,11 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -448,7 +513,11 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
return Option<EmbyShowTitleItemIdResult>.None;
}
private static async Task UpdateShow(TvContext dbContext, EmbyShow existing, EmbyShow incoming)
private static async Task UpdateShow(
TvContext dbContext,
EmbyShow existing,
EmbyShow incoming,
CancellationToken cancellationToken)
{
// library path is used for search indexing later
incoming.LibraryPath = existing.LibraryPath;
@ -589,10 +658,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -589,10 +658,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
metadata.Artwork.Remove(artworkToRemove);
}
await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task UpdateSeason(TvContext dbContext, EmbySeason existing, EmbySeason incoming)
private static async Task UpdateSeason(
TvContext dbContext,
EmbySeason existing,
EmbySeason incoming,
CancellationToken cancellationToken)
{
// library path is used for search indexing later
incoming.LibraryPath = existing.LibraryPath;
@ -684,10 +757,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -684,10 +757,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
metadata.Artwork.Remove(artworkToRemove);
}
await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task UpdateEpisode(TvContext dbContext, EmbyEpisode existing, EmbyEpisode incoming)
private static async Task UpdateEpisode(
TvContext dbContext,
EmbyEpisode existing,
EmbyEpisode incoming,
CancellationToken cancellationToken)
{
// library path is used for search indexing later
incoming.LibraryPath = existing.LibraryPath;
@ -819,13 +896,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -819,13 +896,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
MediaFile incomingFile = incomingVersion.MediaFiles.Head();
file.Path = incomingFile.Path;
await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task<Either<BaseError, MediaItemScanResult<EmbyShow>>> AddShow(
TvContext dbContext,
EmbyLibrary library,
EmbyShow show)
EmbyShow show,
CancellationToken cancellationToken)
{
try
{
@ -835,14 +913,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -835,14 +913,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
show.LibraryPathId = library.Paths.Head().Id;
await dbContext.AddAsync(show);
await dbContext.SaveChangesAsync();
await dbContext.AddAsync(show, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
// restore etag
show.Etag = etag;
await dbContext.Entry(show).Reference(m => m.LibraryPath).LoadAsync();
await dbContext.Entry(show.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(show).Reference(m => m.LibraryPath).LoadAsync(cancellationToken);
await dbContext.Entry(show.LibraryPath).Reference(lp => lp.Library).LoadAsync(cancellationToken);
return new MediaItemScanResult<EmbyShow>(show) { IsAdded = true };
}
catch (Exception ex)
@ -854,7 +932,8 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -854,7 +932,8 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
private static async Task<Either<BaseError, MediaItemScanResult<EmbySeason>>> AddSeason(
TvContext dbContext,
EmbyLibrary library,
EmbySeason season)
EmbySeason season,
CancellationToken cancellationToken)
{
try
{
@ -864,14 +943,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -864,14 +943,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
season.LibraryPathId = library.Paths.Head().Id;
await dbContext.AddAsync(season);
await dbContext.SaveChangesAsync();
await dbContext.AddAsync(season, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
// restore etag
season.Etag = etag;
await dbContext.Entry(season).Reference(m => m.LibraryPath).LoadAsync();
await dbContext.Entry(season.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(season).Reference(m => m.LibraryPath).LoadAsync(cancellationToken);
await dbContext.Entry(season.LibraryPath).Reference(lp => lp.Library).LoadAsync(cancellationToken);
return new MediaItemScanResult<EmbySeason>(season) { IsAdded = true };
}
catch (Exception ex)
@ -892,7 +971,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -892,7 +971,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
episode,
library.Paths.Head().Id,
dbContext,
_logger,
logger,
cancellationToken))
{
return new MediaFileAlreadyExists();

331
ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs

@ -24,46 +24,60 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -24,46 +24,60 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
_logger = logger;
}
public async Task<List<JellyfinItemEtag>> GetExistingShows(JellyfinLibrary library)
public async Task<List<JellyfinItemEtag>> GetExistingShows(
JellyfinLibrary library,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<JellyfinItemEtag>(
@"SELECT ItemId, Etag, MI.State FROM JellyfinShow
new CommandDefinition(
@"SELECT ItemId, Etag, MI.State FROM JellyfinShow
INNER JOIN `Show` S on JellyfinShow.Id = S.Id
INNER JOIN MediaItem MI on S.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id
WHERE LP.LibraryId = @LibraryId",
new { LibraryId = library.Id })
parameters: new { LibraryId = library.Id },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
}
public async Task<List<JellyfinItemEtag>> GetExistingSeasons(JellyfinLibrary library, JellyfinShow show)
public async Task<List<JellyfinItemEtag>> GetExistingSeasons(
JellyfinLibrary library,
JellyfinShow show,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<JellyfinItemEtag>(
@"SELECT JellyfinSeason.ItemId, JellyfinSeason.Etag, MI.State FROM JellyfinSeason
new CommandDefinition(
@"SELECT JellyfinSeason.ItemId, JellyfinSeason.Etag, MI.State FROM JellyfinSeason
INNER JOIN Season S on JellyfinSeason.Id = S.Id
INNER JOIN MediaItem MI on S.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id
INNER JOIN `Show` S2 on S.ShowId = S2.Id
INNER JOIN JellyfinShow JS on S2.Id = JS.Id
WHERE LP.LibraryId = @LibraryId AND JS.ItemId = @ShowItemId",
new { LibraryId = library.Id, ShowItemId = show.ItemId })
parameters: new { LibraryId = library.Id, ShowItemId = show.ItemId },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
}
public async Task<List<JellyfinItemEtag>> GetExistingEpisodes(JellyfinLibrary library, JellyfinSeason season)
public async Task<List<JellyfinItemEtag>> GetExistingEpisodes(
JellyfinLibrary library,
JellyfinSeason season,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<JellyfinItemEtag>(
@"SELECT JellyfinEpisode.ItemId, JellyfinEpisode.Etag, MI.State FROM JellyfinEpisode
new CommandDefinition(
@"SELECT JellyfinEpisode.ItemId, JellyfinEpisode.Etag, MI.State FROM JellyfinEpisode
INNER JOIN Episode E on JellyfinEpisode.Id = E.Id
INNER JOIN MediaItem MI on E.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id
INNER JOIN Season S2 on E.SeasonId = S2.Id
INNER JOIN JellyfinSeason JS on S2.Id = JS.Id
WHERE LP.LibraryId = @LibraryId AND JS.ItemId = @SeasonItemId",
new { LibraryId = library.Id, SeasonItemId = season.ItemId })
parameters: new { LibraryId = library.Id, SeasonItemId = season.ItemId },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
}
@ -97,14 +111,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -97,14 +111,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
var result = new MediaItemScanResult<JellyfinShow>(jellyfinShow) { IsAdded = false };
if (jellyfinShow.Etag != item.Etag)
{
await UpdateShow(dbContext, jellyfinShow, item);
await UpdateShow(dbContext, jellyfinShow, item, cancellationToken);
result.IsUpdated = true;
}
return result;
}
return await AddShow(dbContext, library, item);
return await AddShow(dbContext, library, item, cancellationToken);
}
public async Task<Either<BaseError, MediaItemScanResult<JellyfinSeason>>> GetOrAdd(
@ -126,14 +140,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -126,14 +140,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
var result = new MediaItemScanResult<JellyfinSeason>(jellyfinSeason) { IsAdded = false };
if (jellyfinSeason.Etag != item.Etag)
{
await UpdateSeason(dbContext, jellyfinSeason, item);
await UpdateSeason(dbContext, jellyfinSeason, item, cancellationToken);
result.IsUpdated = true;
}
return result;
}
return await AddSeason(dbContext, library, item);
return await AddSeason(dbContext, library, item, cancellationToken);
}
public async Task<Either<BaseError, MediaItemScanResult<JellyfinEpisode>>> GetOrAdd(
@ -178,7 +192,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -178,7 +192,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
var result = new MediaItemScanResult<JellyfinEpisode>(jellyfinEpisode) { IsAdded = false };
if (jellyfinEpisode.Etag != item.Etag || deepScan)
{
await UpdateEpisode(dbContext, jellyfinEpisode, item);
await UpdateEpisode(dbContext, jellyfinEpisode, item, cancellationToken);
result.IsUpdated = true;
}
@ -188,254 +202,319 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -188,254 +202,319 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
return await AddEpisode(dbContext, library, item, cancellationToken);
}
public async Task<Unit> SetEtag(JellyfinShow show, string etag)
public async Task<Unit> SetEtag(JellyfinShow show, string etag, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.ExecuteAsync(
"UPDATE JellyfinShow SET Etag = @Etag WHERE Id = @Id",
new { Etag = etag, show.Id }).Map(_ => Unit.Default);
new CommandDefinition(
"UPDATE JellyfinShow SET Etag = @Etag WHERE Id = @Id",
parameters: new { Etag = etag, show.Id },
cancellationToken: cancellationToken)).Map(_ => Unit.Default);
}
public async Task<Unit> SetEtag(JellyfinSeason season, string etag)
public async Task<Unit> SetEtag(JellyfinSeason season, string etag, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.ExecuteAsync(
"UPDATE JellyfinSeason SET Etag = @Etag WHERE Id = @Id",
new { Etag = etag, season.Id }).Map(_ => Unit.Default);
new CommandDefinition(
"UPDATE JellyfinSeason SET Etag = @Etag WHERE Id = @Id",
parameters: new { Etag = etag, season.Id },
cancellationToken: cancellationToken)).Map(_ => Unit.Default);
}
public async Task<Unit> SetEtag(JellyfinEpisode episode, string etag)
public async Task<Unit> SetEtag(JellyfinEpisode episode, string etag, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.ExecuteAsync(
"UPDATE JellyfinEpisode SET Etag = @Etag WHERE Id = @Id",
new { Etag = etag, episode.Id }).Map(_ => Unit.Default);
new CommandDefinition(
"UPDATE JellyfinEpisode SET Etag = @Etag WHERE Id = @Id",
parameters: new { Etag = etag, episode.Id },
cancellationToken: cancellationToken)).Map(_ => Unit.Default);
}
public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinEpisode episode)
public async Task<Option<int>> FlagNormal(
JellyfinLibrary library,
JellyfinEpisode episode,
CancellationToken cancellationToken)
{
if (episode.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
episode.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinEpisode.Id FROM JellyfinEpisode
INNER JOIN MediaItem MI ON MI.Id = JellyfinEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinEpisode.ItemId = @ItemId",
new { LibraryId = library.Id, episode.ItemId });
new CommandDefinition(
@"SELECT JellyfinEpisode.Id FROM JellyfinEpisode
INNER JOIN MediaItem MI ON MI.Id = JellyfinEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinEpisode.ItemId = @ItemId",
parameters: new { LibraryId = library.Id, episode.ItemId },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinSeason season)
public async Task<Option<int>> FlagNormal(
JellyfinLibrary library,
JellyfinSeason season,
CancellationToken cancellationToken)
{
if (season.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
season.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinSeason.Id FROM JellyfinSeason
INNER JOIN MediaItem MI ON MI.Id = JellyfinSeason.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinSeason.ItemId = @ItemId",
new { LibraryId = library.Id, season.ItemId });
new CommandDefinition(
@"SELECT JellyfinSeason.Id FROM JellyfinSeason
INNER JOIN MediaItem MI ON MI.Id = JellyfinSeason.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinSeason.ItemId = @ItemId",
parameters: new { LibraryId = library.Id, season.ItemId },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinShow show)
public async Task<Option<int>> FlagNormal(
JellyfinLibrary library,
JellyfinShow show,
CancellationToken cancellationToken)
{
if (show.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
show.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinShow.Id FROM JellyfinShow
INNER JOIN MediaItem MI ON MI.Id = JellyfinShow.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinShow.ItemId = @ItemId",
new { LibraryId = library.Id, show.ItemId });
new CommandDefinition(
@"SELECT JellyfinShow.Id FROM JellyfinShow
INNER JOIN MediaItem MI ON MI.Id = JellyfinShow.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinShow.ItemId = @ItemId",
parameters: new { LibraryId = library.Id, show.ItemId },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<List<int>> FlagFileNotFoundShows(JellyfinLibrary library, List<string> showItemIds)
public async Task<List<int>> FlagFileNotFoundShows(
JellyfinLibrary library,
List<string> showItemIds,
CancellationToken cancellationToken)
{
if (showItemIds.Count == 0)
{
return [];
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN JellyfinShow ON JellyfinShow.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE JellyfinShow.ItemId IN @ShowItemIds",
new { LibraryId = library.Id, ShowItemIds = showItemIds })
new CommandDefinition(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN JellyfinShow ON JellyfinShow.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE JellyfinShow.ItemId IN @ShowItemIds",
parameters: new { LibraryId = library.Id, ShowItemIds = showItemIds },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids });
new CommandDefinition(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
parameters: new { Ids = ids },
cancellationToken: cancellationToken));
return ids;
}
public async Task<List<int>> FlagFileNotFoundSeasons(JellyfinLibrary library, List<string> seasonItemIds)
public async Task<List<int>> FlagFileNotFoundSeasons(
JellyfinLibrary library,
List<string> seasonItemIds,
CancellationToken cancellationToken)
{
if (seasonItemIds.Count == 0)
{
return [];
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id
new CommandDefinition(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN JellyfinSeason ON JellyfinSeason.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE JellyfinSeason.ItemId IN @SeasonItemIds",
new { LibraryId = library.Id, SeasonItemIds = seasonItemIds })
parameters: new { LibraryId = library.Id, SeasonItemIds = seasonItemIds },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids });
new CommandDefinition(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
parameters: new { Ids = ids },
cancellationToken: cancellationToken));
return ids;
}
public async Task<List<int>> FlagFileNotFoundEpisodes(JellyfinLibrary library, List<string> episodeItemIds)
public async Task<List<int>> FlagFileNotFoundEpisodes(
JellyfinLibrary library,
List<string> episodeItemIds,
CancellationToken cancellationToken)
{
if (episodeItemIds.Count == 0)
{
return [];
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN JellyfinEpisode ON JellyfinEpisode.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE JellyfinEpisode.ItemId IN @EpisodeItemIds",
new { LibraryId = library.Id, EpisodeItemIds = episodeItemIds })
new CommandDefinition(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN JellyfinEpisode ON JellyfinEpisode.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE JellyfinEpisode.ItemId IN @EpisodeItemIds",
parameters: new { LibraryId = library.Id, EpisodeItemIds = episodeItemIds },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids });
new CommandDefinition(
"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
parameters: new { Ids = ids },
cancellationToken: cancellationToken));
return ids;
}
public async Task<Option<int>> FlagUnavailable(JellyfinLibrary library, JellyfinEpisode episode)
public async Task<Option<int>> FlagUnavailable(
JellyfinLibrary library,
JellyfinEpisode episode,
CancellationToken cancellationToken)
{
if (episode.State is MediaItemState.Unavailable)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
episode.State = MediaItemState.Unavailable;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinEpisode.Id FROM JellyfinEpisode
INNER JOIN MediaItem MI ON MI.Id = JellyfinEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinEpisode.ItemId = @ItemId",
new { LibraryId = library.Id, episode.ItemId });
new CommandDefinition(
@"SELECT JellyfinEpisode.Id FROM JellyfinEpisode
INNER JOIN MediaItem MI ON MI.Id = JellyfinEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinEpisode.ItemId = @ItemId",
parameters: new { LibraryId = library.Id, episode.ItemId },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<int>> FlagRemoteOnly(JellyfinLibrary library, JellyfinEpisode episode)
public async Task<Option<int>> FlagRemoteOnly(
JellyfinLibrary library,
JellyfinEpisode episode,
CancellationToken cancellationToken)
{
if (episode.State is MediaItemState.RemoteOnly)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
episode.State = MediaItemState.RemoteOnly;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinEpisode.Id FROM JellyfinEpisode
INNER JOIN MediaItem MI ON MI.Id = JellyfinEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinEpisode.ItemId = @ItemId",
new { LibraryId = library.Id, episode.ItemId });
new CommandDefinition(
@"SELECT JellyfinEpisode.Id FROM JellyfinEpisode
INNER JOIN MediaItem MI ON MI.Id = JellyfinEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinEpisode.ItemId = @ItemId",
parameters: new { LibraryId = library.Id, episode.ItemId },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<JellyfinShowTitleItemIdResult>> GetShowTitleItemId(int libraryId, int showId)
public async Task<Option<JellyfinShowTitleItemIdResult>> GetShowTitleItemId(
int libraryId,
int showId,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<JellyfinShow> maybeShow = await dbContext.JellyfinShows
.Where(s => s.Id == showId)
.Where(s => s.LibraryPath.LibraryId == libraryId)
.Include(s => s.ShowMetadata)
.FirstOrDefaultAsync()
.FirstOrDefaultAsync(cancellationToken)
.Map(Optional);
foreach (JellyfinShow show in maybeShow)
@ -448,7 +527,11 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -448,7 +527,11 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
return Option<JellyfinShowTitleItemIdResult>.None;
}
private static async Task UpdateShow(TvContext dbContext, JellyfinShow existing, JellyfinShow incoming)
private static async Task UpdateShow(
TvContext dbContext,
JellyfinShow existing,
JellyfinShow incoming,
CancellationToken cancellationToken)
{
// library path is used for search indexing later
incoming.LibraryPath = existing.LibraryPath;
@ -589,10 +672,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -589,10 +672,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
metadata.Artwork.Remove(artworkToRemove);
}
await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task UpdateSeason(TvContext dbContext, JellyfinSeason existing, JellyfinSeason incoming)
private static async Task UpdateSeason(
TvContext dbContext,
JellyfinSeason existing,
JellyfinSeason incoming,
CancellationToken cancellationToken)
{
// library path is used for search indexing later
incoming.LibraryPath = existing.LibraryPath;
@ -684,10 +771,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -684,10 +771,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
metadata.Artwork.Remove(artworkToRemove);
}
await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task UpdateEpisode(TvContext dbContext, JellyfinEpisode existing, JellyfinEpisode incoming)
private static async Task UpdateEpisode(
TvContext dbContext,
JellyfinEpisode existing,
JellyfinEpisode incoming,
CancellationToken cancellationToken)
{
// library path is used for search indexing later
incoming.LibraryPath = existing.LibraryPath;
@ -819,13 +910,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -819,13 +910,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
MediaFile incomingFile = incomingVersion.MediaFiles.Head();
file.Path = incomingFile.Path;
await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task<Either<BaseError, MediaItemScanResult<JellyfinShow>>> AddShow(
TvContext dbContext,
JellyfinLibrary library,
JellyfinShow show)
JellyfinShow show,
CancellationToken cancellationToken)
{
try
{
@ -835,14 +927,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -835,14 +927,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
show.LibraryPathId = library.Paths.Head().Id;
await dbContext.AddAsync(show);
await dbContext.SaveChangesAsync();
await dbContext.AddAsync(show, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
// restore etag
show.Etag = etag;
await dbContext.Entry(show).Reference(m => m.LibraryPath).LoadAsync();
await dbContext.Entry(show.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(show).Reference(m => m.LibraryPath).LoadAsync(cancellationToken);
await dbContext.Entry(show.LibraryPath).Reference(lp => lp.Library).LoadAsync(cancellationToken);
return new MediaItemScanResult<JellyfinShow>(show) { IsAdded = true };
}
catch (Exception ex)
@ -854,7 +946,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -854,7 +946,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
private static async Task<Either<BaseError, MediaItemScanResult<JellyfinSeason>>> AddSeason(
TvContext dbContext,
JellyfinLibrary library,
JellyfinSeason season)
JellyfinSeason season,
CancellationToken cancellationToken)
{
try
{
@ -864,14 +957,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -864,14 +957,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
season.LibraryPathId = library.Paths.Head().Id;
await dbContext.AddAsync(season);
await dbContext.SaveChangesAsync();
await dbContext.AddAsync(season, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
// restore etag
season.Etag = etag;
await dbContext.Entry(season).Reference(m => m.LibraryPath).LoadAsync();
await dbContext.Entry(season.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(season).Reference(m => m.LibraryPath).LoadAsync(cancellationToken);
await dbContext.Entry(season.LibraryPath).Reference(lp => lp.Library).LoadAsync(cancellationToken);
return new MediaItemScanResult<JellyfinSeason>(season) { IsAdded = true };
}
catch (Exception ex)

372
ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs

@ -25,183 +25,227 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -25,183 +25,227 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
_logger = logger;
}
public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexEpisode episode)
public async Task<Option<int>> FlagNormal(
PlexLibrary library,
PlexEpisode episode,
CancellationToken cancellationToken)
{
if (episode.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
episode.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT PlexEpisode.Id FROM PlexEpisode
INNER JOIN MediaItem MI ON MI.Id = PlexEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexEpisode.Key = @Key",
new { LibraryId = library.Id, episode.Key });
new CommandDefinition(
@"SELECT PlexEpisode.Id FROM PlexEpisode
INNER JOIN MediaItem MI ON MI.Id = PlexEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexEpisode.Key = @Key",
parameters: new { LibraryId = library.Id, episode.Key },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexSeason season)
public async Task<Option<int>> FlagNormal(
PlexLibrary library,
PlexSeason season,
CancellationToken cancellationToken)
{
if (season.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
season.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT PlexSeason.Id FROM PlexSeason
INNER JOIN MediaItem MI ON MI.Id = PlexSeason.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexSeason.Key = @Key",
new { LibraryId = library.Id, season.Key });
new CommandDefinition(
@"SELECT PlexSeason.Id FROM PlexSeason
INNER JOIN MediaItem MI ON MI.Id = PlexSeason.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexSeason.Key = @Key",
parameters: new { LibraryId = library.Id, season.Key },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexShow show)
public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexShow show, CancellationToken cancellationToken)
{
if (show.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
show.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT PlexShow.Id FROM PlexShow
INNER JOIN MediaItem MI ON MI.Id = PlexShow.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexShow.Key = @Key",
new { LibraryId = library.Id, show.Key });
new CommandDefinition(
@"SELECT PlexShow.Id FROM PlexShow
INNER JOIN MediaItem MI ON MI.Id = PlexShow.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexShow.Key = @Key",
parameters: new { LibraryId = library.Id, show.Key },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<int>> FlagUnavailable(PlexLibrary library, PlexEpisode episode)
public async Task<Option<int>> FlagUnavailable(
PlexLibrary library,
PlexEpisode episode,
CancellationToken cancellationToken)
{
if (episode.State is MediaItemState.Unavailable)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
episode.State = MediaItemState.Unavailable;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT PlexEpisode.Id FROM PlexEpisode
INNER JOIN MediaItem MI ON MI.Id = PlexEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexEpisode.Key = @Key",
new { LibraryId = library.Id, episode.Key });
new CommandDefinition(
@"SELECT PlexEpisode.Id FROM PlexEpisode
INNER JOIN MediaItem MI ON MI.Id = PlexEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexEpisode.Key = @Key",
parameters: new { LibraryId = library.Id, episode.Key },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<Option<int>> FlagRemoteOnly(PlexLibrary library, PlexEpisode episode)
public async Task<Option<int>> FlagRemoteOnly(
PlexLibrary library,
PlexEpisode episode,
CancellationToken cancellationToken)
{
if (episode.State is MediaItemState.RemoteOnly)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
episode.State = MediaItemState.RemoteOnly;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT PlexEpisode.Id FROM PlexEpisode
INNER JOIN MediaItem MI ON MI.Id = PlexEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexEpisode.Key = @Key",
new { LibraryId = library.Id, episode.Key });
new CommandDefinition(
@"SELECT PlexEpisode.Id FROM PlexEpisode
INNER JOIN MediaItem MI ON MI.Id = PlexEpisode.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexEpisode.Key = @Key",
parameters: new { LibraryId = library.Id, episode.Key },
cancellationToken: cancellationToken));
foreach (int id in maybeId)
{
return await dbContext.Connection.ExecuteAsync(
"UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
new { Id = id }).Map(count => count > 0 ? Some(id) : None);
new CommandDefinition(
"UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
parameters: new { Id = id },
cancellationToken: cancellationToken)).Map(count => count > 0 ? Some(id) : None);
}
return None;
}
public async Task<List<PlexItemEtag>> GetExistingShows(PlexLibrary library)
public async Task<List<PlexItemEtag>> GetExistingShows(PlexLibrary library, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<PlexItemEtag>(
@"SELECT PS.Key, PS.Etag, MI.State FROM PlexShow PS
new CommandDefinition(
@"SELECT PS.Key, PS.Etag, MI.State FROM PlexShow PS
INNER JOIN MediaItem MI on PS.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId",
new { LibraryId = library.Id })
parameters: new { LibraryId = library.Id },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
}
public async Task<List<PlexItemEtag>> GetExistingSeasons(PlexLibrary library, PlexShow show)
public async Task<List<PlexItemEtag>> GetExistingSeasons(
PlexLibrary library,
PlexShow show,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<PlexItemEtag>(
@"SELECT PlexSeason.Key, PlexSeason.Etag, MI.State FROM PlexSeason
new CommandDefinition(
@"SELECT PlexSeason.Key, PlexSeason.Etag, MI.State FROM PlexSeason
INNER JOIN Season S on PlexSeason.Id = S.Id
INNER JOIN MediaItem MI on S.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
INNER JOIN PlexShow PS ON S.ShowId = PS.Id
WHERE LP.LibraryId = @LibraryId AND PS.Key = @Key",
new { LibraryId = library.Id, show.Key })
parameters: new { LibraryId = library.Id, show.Key },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
}
public async Task<List<PlexItemEtag>> GetExistingEpisodes(PlexLibrary library, PlexSeason season)
public async Task<List<PlexItemEtag>> GetExistingEpisodes(
PlexLibrary library,
PlexSeason season,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<PlexItemEtag>(
@"SELECT PlexEpisode.Key, PlexEpisode.Etag, MI.State FROM PlexEpisode
new CommandDefinition(
@"SELECT PlexEpisode.Key, PlexEpisode.Etag, MI.State FROM PlexEpisode
INNER JOIN Episode E on PlexEpisode.Id = E.Id
INNER JOIN MediaItem MI on E.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id
INNER JOIN Season S2 on E.SeasonId = S2.Id
INNER JOIN PlexSeason PS on S2.Id = PS.Id
WHERE LP.LibraryId = @LibraryId AND PS.Key = @Key",
new { LibraryId = library.Id, season.Key })
parameters: new { LibraryId = library.Id, season.Key },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
}
@ -238,7 +282,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -238,7 +282,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
return new MediaItemScanResult<PlexShow>(plexShow) { IsAdded = false };
}
return await AddShow(dbContext, library, item);
return await AddShow(dbContext, library, item, cancellationToken);
}
public async Task<Either<BaseError, MediaItemScanResult<PlexSeason>>> GetOrAdd(
@ -267,7 +311,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -267,7 +311,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
return new MediaItemScanResult<PlexSeason>(plexSeason) { IsAdded = false };
}
return await AddSeason(dbContext, library, item);
return await AddSeason(dbContext, library, item, cancellationToken);
}
public async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> GetOrAdd(
@ -313,7 +357,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -313,7 +357,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
var result = new MediaItemScanResult<PlexEpisode>(plexEpisode) { IsAdded = false };
if (plexEpisode.Etag != item.Etag || deepScan)
{
foreach (BaseError error in await UpdateEpisodePath(dbContext, plexEpisode, item))
foreach (BaseError error in await UpdateEpisodePath(dbContext, plexEpisode, item, cancellationToken))
{
return error;
}
@ -327,101 +371,128 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -327,101 +371,128 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
return await AddEpisode(dbContext, library, item, cancellationToken);
}
public async Task<Unit> SetEtag(PlexShow show, string etag)
public async Task<Unit> SetEtag(PlexShow show, string etag, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.ExecuteAsync(
"UPDATE PlexShow SET Etag = @Etag WHERE Id = @Id",
new { Etag = etag, show.Id }).Map(_ => Unit.Default);
new CommandDefinition(
"UPDATE PlexShow SET Etag = @Etag WHERE Id = @Id",
parameters: new { Etag = etag, show.Id },
cancellationToken: cancellationToken)).Map(_ => Unit.Default);
}
public async Task<Unit> SetEtag(PlexSeason season, string etag)
public async Task<Unit> SetEtag(PlexSeason season, string etag, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.ExecuteAsync(
"UPDATE PlexSeason SET Etag = @Etag WHERE Id = @Id",
new { Etag = etag, season.Id }).Map(_ => Unit.Default);
new CommandDefinition(
"UPDATE PlexSeason SET Etag = @Etag WHERE Id = @Id",
parameters: new { Etag = etag, season.Id },
cancellationToken: cancellationToken)).Map(_ => Unit.Default);
}
public async Task<Unit> SetEtag(PlexEpisode episode, string etag)
public async Task<Unit> SetEtag(PlexEpisode episode, string etag, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.ExecuteAsync(
"UPDATE PlexEpisode SET Etag = @Etag WHERE Id = @Id",
new { Etag = etag, episode.Id }).Map(_ => Unit.Default);
new CommandDefinition(
"UPDATE PlexEpisode SET Etag = @Etag WHERE Id = @Id",
parameters: new { Etag = etag, episode.Id },
cancellationToken: cancellationToken)).Map(_ => Unit.Default);
}
public async Task<List<int>> FlagFileNotFoundShows(PlexLibrary library, List<string> showItemIds)
public async Task<List<int>> FlagFileNotFoundShows(
PlexLibrary library,
List<string> showItemIds,
CancellationToken cancellationToken)
{
if (showItemIds.Count == 0)
{
return [];
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id
new CommandDefinition(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN PlexShow ON PlexShow.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE PlexShow.Key IN @ShowKeys",
new { LibraryId = library.Id, ShowKeys = showItemIds })
parameters: new { LibraryId = library.Id, ShowKeys = showItemIds },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids",
new { Ids = ids });
new CommandDefinition(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids",
parameters: new { Ids = ids },
cancellationToken: cancellationToken));
return ids;
}
public async Task<List<int>> FlagFileNotFoundSeasons(PlexLibrary library, List<string> seasonItemIds)
public async Task<List<int>> FlagFileNotFoundSeasons(
PlexLibrary library,
List<string> seasonItemIds,
CancellationToken cancellationToken)
{
if (seasonItemIds.Count == 0)
{
return [];
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id
new CommandDefinition(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN PlexSeason ON PlexSeason.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE PlexSeason.Key IN @SeasonKeys",
new { LibraryId = library.Id, SeasonKeys = seasonItemIds })
parameters: new { LibraryId = library.Id, SeasonKeys = seasonItemIds },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids",
new { Ids = ids });
new CommandDefinition(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids",
parameters: new { Ids = ids },
cancellationToken: cancellationToken));
return ids;
}
public async Task<List<int>> FlagFileNotFoundEpisodes(PlexLibrary library, List<string> episodeItemIds)
public async Task<List<int>> FlagFileNotFoundEpisodes(
PlexLibrary library,
List<string> episodeItemIds,
CancellationToken cancellationToken)
{
if (episodeItemIds.Count == 0)
{
return [];
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN PlexEpisode ON PlexEpisode.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE PlexEpisode.Key IN @EpisodeKeys",
new { LibraryId = library.Id, EpisodeKeys = episodeItemIds })
new CommandDefinition(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN PlexEpisode ON PlexEpisode.Id = M.Id
INNER JOIN LibraryPath LP on M.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE PlexEpisode.Key IN @EpisodeKeys",
parameters: new { LibraryId = library.Id, EpisodeKeys = episodeItemIds },
cancellationToken: cancellationToken))
.Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids",
new { Ids = ids });
new CommandDefinition(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids",
parameters: new { Ids = ids },
cancellationToken: cancellationToken));
return ids;
}
@ -429,9 +500,10 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -429,9 +500,10 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
public async Task<List<int>> RemoveAllTags(
PlexLibrary library,
PlexTag tag,
System.Collections.Generic.HashSet<int> keep)
System.Collections.Generic.HashSet<int> keep,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
var tagType = tag.TagType.ToString(CultureInfo.InvariantCulture);
@ -440,7 +512,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -440,7 +512,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
.Where(sm => sm.Show.LibraryPath.LibraryId == library.Id)
.Where(sm => sm.Tags.Any(t => t.Name == tag.Tag && t.ExternalTypeId == tagType))
.Select(sm => sm.ShowId)
.ToListAsync();
.ToListAsync(cancellationToken);
if (result.Count > 0)
{
@ -448,28 +520,38 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -448,28 +520,38 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
.Where(sm => result.Contains(sm.ShowId))
.Where(sm => sm.Tags.Any(t => t.Name == tag.Tag && t.ExternalTypeId == tagType))
.SelectMany(sm => sm.Tags.Select(t => t.Id))
.ToListAsync();
.ToListAsync(cancellationToken);
// delete all tags
await dbContext.Connection.ExecuteAsync("DELETE FROM Tag WHERE Id IN @TagIds", new { TagIds = tagIds });
await dbContext.Connection.ExecuteAsync(
new CommandDefinition(
"DELETE FROM Tag WHERE Id IN @TagIds",
parameters: new { TagIds = tagIds },
cancellationToken: cancellationToken));
}
// show ids to refresh
return result;
}
public async Task<PlexShowAddTagResult> AddTag(PlexLibrary library, PlexShow show, PlexTag tag)
public async Task<PlexShowAddTagResult> AddTag(
PlexLibrary library,
PlexShow show,
PlexTag tag,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
int existingShowId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT PS.Id FROM Tag
INNER JOIN ShowMetadata SM on SM.Id = Tag.ShowMetadataId
INNER JOIN PlexShow PS on PS.Id = SM.ShowId
INNER JOIN MediaItem MI on PS.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE PS.Key = @Key AND Tag.Name = @Tag AND Tag.ExternalTypeId = @TagType",
new { show.Key, tag.Tag, tag.TagType, LibraryId = library.Id });
new CommandDefinition(
@"SELECT PS.Id FROM Tag
INNER JOIN ShowMetadata SM on SM.Id = Tag.ShowMetadataId
INNER JOIN PlexShow PS on PS.Id = SM.ShowId
INNER JOIN MediaItem MI on PS.Id = MI.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LP.LibraryId = @LibraryId
WHERE PS.Key = @Key AND Tag.Name = @Tag AND Tag.ExternalTypeId = @TagType",
parameters: new { show.Key, tag.Tag, tag.TagType, LibraryId = library.Id },
cancellationToken: cancellationToken));
// already exists
if (existingShowId > 0)
@ -480,35 +562,42 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -480,35 +562,42 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
int showId = await dbContext.PlexShows
.Where(s => s.Key == show.Key)
.Select(s => s.Id)
.FirstOrDefaultAsync();
.FirstOrDefaultAsync(cancellationToken);
await dbContext.Connection.ExecuteAsync(
@"INSERT INTO Tag (Name, ExternalTypeId, ShowMetadataId)
SELECT @Tag, @TagType, Id FROM
(SELECT Id FROM ShowMetadata WHERE ShowId = @ShowId) AS A",
new { tag.Tag, tag.TagType, ShowId = showId });
new CommandDefinition(
@"INSERT INTO Tag (Name, ExternalTypeId, ShowMetadataId)
SELECT @Tag, @TagType, Id FROM
(SELECT Id FROM ShowMetadata WHERE ShowId = @ShowId) AS A",
parameters: new { tag.Tag, tag.TagType, ShowId = showId },
cancellationToken: cancellationToken));
// show id to refresh
return new PlexShowAddTagResult(Option<int>.None, showId);
}
public async Task UpdateLastNetworksScan(PlexLibrary library)
public async Task UpdateLastNetworksScan(PlexLibrary library, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await dbContext.Connection.ExecuteAsync(
"UPDATE PlexLibrary SET LastNetworksScan = @LastNetworksScan WHERE Id = @Id",
new { library.LastNetworksScan, library.Id });
new CommandDefinition(
"UPDATE PlexLibrary SET LastNetworksScan = @LastNetworksScan WHERE Id = @Id",
parameters: new { library.LastNetworksScan, library.Id },
cancellationToken: cancellationToken));
}
public async Task<Option<PlexShowTitleKeyResult>> GetShowTitleKey(int libraryId, int showId)
public async Task<Option<PlexShowTitleKeyResult>> GetShowTitleKey(
int libraryId,
int showId,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<PlexShow> maybeShow = await dbContext.PlexShows
.Where(s => s.Id == showId)
.Where(s => s.LibraryPath.LibraryId == libraryId)
.Include(s => s.ShowMetadata)
.FirstOrDefaultAsync()
.FirstOrDefaultAsync(cancellationToken)
.Map(Optional);
foreach (PlexShow show in maybeShow)
@ -524,7 +613,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -524,7 +613,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
private static async Task<Either<BaseError, MediaItemScanResult<PlexShow>>> AddShow(
TvContext dbContext,
PlexLibrary library,
PlexShow item)
PlexShow item,
CancellationToken cancellationToken)
{
try
{
@ -534,14 +624,14 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -534,14 +624,14 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
item.LibraryPathId = library.Paths.Head().Id;
await dbContext.PlexShows.AddAsync(item);
await dbContext.SaveChangesAsync();
await dbContext.PlexShows.AddAsync(item, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
// restore etag
item.Etag = etag;
await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync(cancellationToken);
await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync(cancellationToken);
return new MediaItemScanResult<PlexShow>(item) { IsAdded = true };
}
catch (Exception ex)
@ -553,7 +643,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -553,7 +643,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
private static async Task<Either<BaseError, MediaItemScanResult<PlexSeason>>> AddSeason(
TvContext dbContext,
PlexLibrary library,
PlexSeason item)
PlexSeason item,
CancellationToken cancellationToken)
{
try
{
@ -563,14 +654,14 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -563,14 +654,14 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
item.LibraryPathId = library.Paths.Head().Id;
await dbContext.PlexSeasons.AddAsync(item);
await dbContext.SaveChangesAsync();
await dbContext.PlexSeasons.AddAsync(item, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
// restore etag
item.Etag = etag;
await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync(cancellationToken);
await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync(cancellationToken);
return new MediaItemScanResult<PlexSeason>(item) { IsAdded = true };
}
catch (Exception ex)
@ -632,7 +723,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -632,7 +723,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
private async Task<Option<BaseError>> UpdateEpisodePath(
TvContext dbContext,
PlexEpisode existing,
PlexEpisode incoming)
PlexEpisode incoming,
CancellationToken cancellationToken)
{
try
{
@ -663,16 +755,22 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -663,16 +755,22 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
file.Key = incomingFile.Key;
await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaVersion SET Name = @Name, DateAdded = @DateAdded WHERE Id = @Id",
new { version.Name, version.DateAdded, version.Id });
new CommandDefinition(
@"UPDATE MediaVersion SET Name = @Name, DateAdded = @DateAdded WHERE Id = @Id",
parameters: new { version.Name, version.DateAdded, version.Id },
cancellationToken: cancellationToken));
await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaFile SET Path = @Path WHERE Id = @Id",
new { file.Path, file.Id });
new CommandDefinition(
@"UPDATE MediaFile SET Path = @Path WHERE Id = @Id",
parameters: new { file.Path, file.Id },
cancellationToken: cancellationToken));
await dbContext.Connection.ExecuteAsync(
@"UPDATE PlexMediaFile SET `Key` = @Key WHERE Id = @Id",
new { file.Key, file.Id });
new CommandDefinition(
@"UPDATE PlexMediaFile SET `Key` = @Key WHERE Id = @Id",
parameters: new { file.Key, file.Id },
cancellationToken: cancellationToken));
}
return Option<BaseError>.None;

68
ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs

@ -4,6 +4,7 @@ using ErsatzTV.Core.Domain; @@ -4,6 +4,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -54,65 +55,76 @@ public class RemoteStreamRepository( @@ -54,65 +55,76 @@ public class RemoteStreamRepository(
async () => await AddRemoteStream(dbContext, libraryPath.Id, libraryFolder.Id, path, cancellationToken));
}
public async Task<IEnumerable<string>> FindRemoteStreamPaths(LibraryPath libraryPath)
public async Task<IEnumerable<string>> FindRemoteStreamPaths(
LibraryPath libraryPath,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<string>(
@"SELECT MF.Path
new CommandDefinition(
@"SELECT MF.Path
FROM MediaFile MF
INNER JOIN MediaVersion MV on MF.MediaVersionId = MV.Id
INNER JOIN RemoteStream O on MV.RemoteStreamId = O.Id
INNER JOIN MediaItem MI on O.Id = MI.Id
WHERE MI.LibraryPathId = @LibraryPathId",
new { LibraryPathId = libraryPath.Id });
parameters: new { LibraryPathId = libraryPath.Id },
cancellationToken: cancellationToken));
}
public async Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path)
public async Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT O.Id
FROM RemoteStream O
INNER JOIN MediaItem MI on O.Id = MI.Id
INNER JOIN MediaVersion MV on O.Id = MV.RemoteStreamId
INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
new { LibraryPathId = libraryPath.Id, Path = path }).Map(result => result.ToList());
new CommandDefinition(
@"SELECT O.Id
FROM RemoteStream O
INNER JOIN MediaItem MI on O.Id = MI.Id
INNER JOIN MediaVersion MV on O.Id = MV.RemoteStreamId
INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
parameters: new { LibraryPathId = libraryPath.Id, Path = path },
cancellationToken: cancellationToken)).Map(result => result.ToList());
foreach (int remoteStreamId in ids)
{
RemoteStream remoteStream = await dbContext.RemoteStreams.FindAsync(remoteStreamId);
if (remoteStream != null)
Option<RemoteStream> maybeRemoteStream = await dbContext.RemoteStreams
.SelectOneAsync(rs => rs.Id, rs => rs.Id == remoteStreamId, cancellationToken);
foreach (var remoteStream in maybeRemoteStream)
{
dbContext.RemoteStreams.Remove(remoteStream);
}
}
await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
return ids;
}
public async Task<bool> AddTag(RemoteStreamMetadata metadata, Tag tag)
public async Task<bool> AddTag(RemoteStreamMetadata metadata, Tag tag, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.ExecuteAsync(
"INSERT INTO Tag (Name, RemoteStreamMetadataId, ExternalCollectionId) VALUES (@Name, @MetadataId, @ExternalCollectionId)",
new { tag.Name, MetadataId = metadata.Id, tag.ExternalCollectionId }).Map(result => result > 0);
new CommandDefinition(
"INSERT INTO Tag (Name, RemoteStreamMetadataId, ExternalCollectionId) VALUES (@Name, @MetadataId, @ExternalCollectionId)",
parameters: new { tag.Name, MetadataId = metadata.Id, tag.ExternalCollectionId },
cancellationToken: cancellationToken)).Map(result => result > 0);
}
public async Task UpdateDefinition(RemoteStream remoteStream)
public async Task UpdateDefinition(RemoteStream remoteStream, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
await dbContext.RemoteStreams
.Where(rs => rs.Id == remoteStream.Id)
.ExecuteUpdateAsync(setters => setters
.SetProperty(rs => rs.Url, remoteStream.Url)
.SetProperty(rs => rs.Script, remoteStream.Script)
.SetProperty(rs => rs.Duration, remoteStream.Duration)
.SetProperty(rs => rs.FallbackQuery, remoteStream.FallbackQuery)
.SetProperty(rs => rs.IsLive, remoteStream.IsLive));
.ExecuteUpdateAsync(
setters => setters
.SetProperty(rs => rs.Url, remoteStream.Url)
.SetProperty(rs => rs.Script, remoteStream.Script)
.SetProperty(rs => rs.Duration, remoteStream.Duration)
.SetProperty(rs => rs.FallbackQuery, remoteStream.FallbackQuery)
.SetProperty(rs => rs.IsLive, remoteStream.IsLive),
cancellationToken);
}
private async Task<Either<BaseError, MediaItemScanResult<RemoteStream>>> AddRemoteStream(

7
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs

@ -74,7 +74,7 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho @@ -74,7 +74,7 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho
SynchronizeEmbyShowById request,
CancellationToken cancellationToken) =>
(await ValidateConnection(request), await EmbyLibraryMustExist(request, cancellationToken),
await EmbyShowMustExist(request))
await EmbyShowMustExist(request, cancellationToken))
.Apply((connectionParameters, embyLibrary, showTitleItemId) =>
new RequestParameters(
connectionParameters,
@ -121,8 +121,9 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho @@ -121,8 +121,9 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler<SynchronizeEmbySho
.Map(v => v.ToValidation<BaseError>($"Emby library {request.EmbyLibraryId} does not exist."));
private Task<Validation<BaseError, EmbyShowTitleItemIdResult>> EmbyShowMustExist(
SynchronizeEmbyShowById request) =>
_embyTelevisionRepository.GetShowTitleItemId(request.EmbyLibraryId, request.ShowId)
SynchronizeEmbyShowById request,
CancellationToken cancellationToken) =>
_embyTelevisionRepository.GetShowTitleItemId(request.EmbyLibraryId, request.ShowId, cancellationToken)
.Map(v => v.ToValidation<BaseError>(
$"Jellyfin show {request.ShowId} does not exist in library {request.EmbyLibraryId}."));

13
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs

@ -34,7 +34,7 @@ public class @@ -34,7 +34,7 @@ public class
SynchronizeJellyfinShowById request,
CancellationToken cancellationToken)
{
Validation<BaseError, RequestParameters> validation = await Validate(request);
Validation<BaseError, RequestParameters> validation = await Validate(request, cancellationToken);
return await validation.Match(
parameters => Synchronize(parameters, cancellationToken),
error => Task.FromResult<Either<BaseError, string>>(error.Join()));
@ -71,9 +71,11 @@ public class @@ -71,9 +71,11 @@ public class
return result.Map(_ => $"Show '{parameters.ShowTitle}' in {parameters.Library.Name}");
}
private async Task<Validation<BaseError, RequestParameters>> Validate(SynchronizeJellyfinShowById request) =>
private async Task<Validation<BaseError, RequestParameters>> Validate(
SynchronizeJellyfinShowById request,
CancellationToken cancellationToken) =>
(await ValidateConnection(request), await JellyfinLibraryMustExist(request),
await JellyfinShowMustExist(request))
await JellyfinShowMustExist(request, cancellationToken))
.Apply((connectionParameters, jellyfinLibrary, showTitleItemId) =>
new RequestParameters(
connectionParameters,
@ -119,8 +121,9 @@ public class @@ -119,8 +121,9 @@ public class
.Map(v => v.ToValidation<BaseError>($"Jellyfin library {request.JellyfinLibraryId} does not exist."));
private Task<Validation<BaseError, JellyfinShowTitleItemIdResult>> JellyfinShowMustExist(
SynchronizeJellyfinShowById request) =>
_jellyfinTelevisionRepository.GetShowTitleItemId(request.JellyfinLibraryId, request.ShowId)
SynchronizeJellyfinShowById request,
CancellationToken cancellationToken) =>
_jellyfinTelevisionRepository.GetShowTitleItemId(request.JellyfinLibraryId, request.ShowId, cancellationToken)
.Map(v => v.ToValidation<BaseError>(
$"Jellyfin show {request.ShowId} does not exist in library {request.JellyfinLibraryId}."));

2
ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexNetworksHandler.cs

@ -107,7 +107,7 @@ public class SynchronizePlexNetworksHandler : IRequestHandler<SynchronizePlexNet @@ -107,7 +107,7 @@ public class SynchronizePlexNetworksHandler : IRequestHandler<SynchronizePlexNet
if (result.IsRight)
{
parameters.Library.LastNetworksScan = DateTime.UtcNow;
await _plexTelevisionRepository.UpdateLastNetworksScan(parameters.Library);
await _plexTelevisionRepository.UpdateLastNetworksScan(parameters.Library, cancellationToken);
}
return result;

14
ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexShowByIdHandler.cs

@ -33,7 +33,7 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho @@ -33,7 +33,7 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho
SynchronizePlexShowById request,
CancellationToken cancellationToken)
{
Validation<BaseError, RequestParameters> validation = await Validate(request);
Validation<BaseError, RequestParameters> validation = await Validate(request, cancellationToken);
return await validation.Match(
parameters => Synchronize(parameters, cancellationToken),
error => Task.FromResult<Either<BaseError, string>>(error.Join()));
@ -70,8 +70,11 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho @@ -70,8 +70,11 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho
return result.Map(_ => $"Show '{parameters.ShowTitle}' in {parameters.Library.Name}");
}
private async Task<Validation<BaseError, RequestParameters>> Validate(SynchronizePlexShowById request) =>
(await ValidateConnection(request), await PlexLibraryMustExist(request), await PlexShowMustExist(request))
private async Task<Validation<BaseError, RequestParameters>> Validate(
SynchronizePlexShowById request,
CancellationToken cancellationToken) =>
(await ValidateConnection(request), await PlexLibraryMustExist(request),
await PlexShowMustExist(request, cancellationToken))
.Apply((connectionParameters, plexLibrary, titleKey) =>
new RequestParameters(
connectionParameters,
@ -117,8 +120,9 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho @@ -117,8 +120,9 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler<SynchronizePlexSho
.Map(v => v.ToValidation<BaseError>($"Plex library {request.PlexLibraryId} does not exist."));
private Task<Validation<BaseError, PlexShowTitleKeyResult>> PlexShowMustExist(
SynchronizePlexShowById request) =>
_plexTelevisionRepository.GetShowTitleKey(request.PlexLibraryId, request.ShowId)
SynchronizePlexShowById request,
CancellationToken cancellationToken) =>
_plexTelevisionRepository.GetShowTitleKey(request.PlexLibraryId, request.ShowId, cancellationToken)
.Map(v => v.ToValidation<BaseError>(
$"Plex show {request.ShowId} does not exist in library {request.PlexLibraryId}."));

4
ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs

@ -14,7 +14,7 @@ public interface ILocalMetadataProvider @@ -14,7 +14,7 @@ public interface ILocalMetadataProvider
Task<bool> RefreshSidecarMetadata(OtherVideo otherVideo, string nfoFileName);
Task<bool> RefreshTagMetadata(Song song);
Task<bool> RefreshTagMetadata(Image image, double? durationSeconds);
Task<bool> RefreshTagMetadata(RemoteStream remoteStream);
Task<bool> RefreshTagMetadata(RemoteStream remoteStream, CancellationToken cancellationToken);
Task<bool> RefreshFallbackMetadata(Movie movie);
Task<bool> RefreshFallbackMetadata(Episode episode);
Task<bool> RefreshFallbackMetadata(Artist artist, string artistFolder);
@ -22,6 +22,6 @@ public interface ILocalMetadataProvider @@ -22,6 +22,6 @@ public interface ILocalMetadataProvider
Task<bool> RefreshFallbackMetadata(OtherVideo otherVideo);
Task<bool> RefreshFallbackMetadata(Song song);
Task<bool> RefreshFallbackMetadata(Image image);
Task<bool> RefreshFallbackMetadata(RemoteStream remoteStream);
Task<bool> RefreshFallbackMetadata(RemoteStream remoteStream, CancellationToken cancellationToken);
Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder);
}

15
ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs

@ -224,13 +224,13 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -224,13 +224,13 @@ public class LocalMetadataProvider : ILocalMetadataProvider
return await RefreshFallbackMetadata(image);
}
public async Task<bool> RefreshTagMetadata(RemoteStream remoteStream) =>
public async Task<bool> RefreshTagMetadata(RemoteStream remoteStream, CancellationToken cancellationToken) =>
// Option<RemoteStreamMetadata> maybeMetadata = LoadRemoteStreamMetadata(remoteStream);
// foreach (RemoteStreamMetadata metadata in maybeMetadata)
// {
// return await ApplyMetadataUpdate(remoteStream, metadata);
// }
await RefreshFallbackMetadata(remoteStream);
await RefreshFallbackMetadata(remoteStream, cancellationToken);
public Task<bool> RefreshFallbackMetadata(Movie movie) =>
ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie));
@ -274,12 +274,12 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -274,12 +274,12 @@ public class LocalMetadataProvider : ILocalMetadataProvider
return false;
}
public async Task<bool> RefreshFallbackMetadata(RemoteStream remoteStream)
public async Task<bool> RefreshFallbackMetadata(RemoteStream remoteStream, CancellationToken cancellationToken)
{
Option<RemoteStreamMetadata> maybeMetadata = _fallbackMetadataProvider.GetFallbackMetadata(remoteStream);
foreach (RemoteStreamMetadata metadata in maybeMetadata)
{
return await ApplyMetadataUpdate(remoteStream, metadata);
return await ApplyMetadataUpdate(remoteStream, metadata, cancellationToken);
}
return false;
@ -1239,7 +1239,10 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -1239,7 +1239,10 @@ public class LocalMetadataProvider : ILocalMetadataProvider
return await _metadataRepository.Add(metadata);
}
private async Task<bool> ApplyMetadataUpdate(RemoteStream remoteStream, RemoteStreamMetadata metadata)
private async Task<bool> ApplyMetadataUpdate(
RemoteStream remoteStream,
RemoteStreamMetadata metadata,
CancellationToken cancellationToken)
{
Option<RemoteStreamMetadata> maybeMetadata = Optional(remoteStream.RemoteStreamMetadata).Flatten().HeadOrNone();
foreach (RemoteStreamMetadata existing in maybeMetadata)
@ -1265,7 +1268,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -1265,7 +1268,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
existing,
metadata,
(_, _) => Task.FromResult(false),
_remoteStreamRepository.AddTag,
(metadata1, tag) => _remoteStreamRepository.AddTag(metadata1, tag, cancellationToken),
(_, _) => Task.FromResult(false),
(_, _) => Task.FromResult(false));

42
ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -91,7 +91,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -91,7 +91,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
CancellationToken cancellationToken)
{
var incomingItemIds = new List<string>();
List<TEtag> existingShows = await televisionRepository.GetExistingShows(library);
List<TEtag> existingShows = await televisionRepository.GetExistingShows(library, cancellationToken);
await foreach ((TShow incoming, int totalShowCount) in showEntries.WithCancellation(cancellationToken))
{
@ -147,9 +147,9 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -147,9 +147,9 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
return error;
}
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming));
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming), cancellationToken);
Option<int> flagResult = await televisionRepository.FlagNormal(library, result.Item);
Option<int> flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken);
if (flagResult.IsSome)
{
result.IsUpdated = true;
@ -173,7 +173,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -173,7 +173,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{
// trash shows that are no longer present on the media server
var fileNotFoundItemIds = existingShows.Map(s => s.MediaServerItemId).Except(incomingItemIds).ToList();
List<int> ids = await televisionRepository.FlagFileNotFoundShows(library, fileNotFoundItemIds);
List<int> ids = await televisionRepository.FlagFileNotFoundShows(library, fileNotFoundItemIds, cancellationToken);
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty<int>()),
cancellationToken);
@ -296,7 +296,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -296,7 +296,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
CancellationToken cancellationToken)
{
var incomingItemIds = new List<string>();
List<TEtag> existingSeasons = await televisionRepository.GetExistingSeasons(library, show);
List<TEtag> existingSeasons = await televisionRepository.GetExistingSeasons(library, show, cancellationToken);
await foreach ((TSeason incoming, int _) in seasonEntries.WithCancellation(cancellationToken))
{
@ -346,9 +346,9 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -346,9 +346,9 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
return error;
}
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming));
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming), cancellationToken);
Option<int> flagResult = await televisionRepository.FlagNormal(library, result.Item);
Option<int> flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken);
if (flagResult.IsSome)
{
result.IsUpdated = true;
@ -372,7 +372,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -372,7 +372,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
// trash seasons that are no longer present on the media server
var fileNotFoundItemIds = existingSeasons.Map(s => s.MediaServerItemId).Except(incomingItemIds).ToList();
List<int> ids = await televisionRepository.FlagFileNotFoundSeasons(library, fileNotFoundItemIds);
List<int> ids = await televisionRepository.FlagFileNotFoundSeasons(library, fileNotFoundItemIds, cancellationToken);
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty<int>()),
cancellationToken);
@ -393,7 +393,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -393,7 +393,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
CancellationToken cancellationToken)
{
var incomingItemIds = new List<string>();
List<TEtag> existingEpisodes = await televisionRepository.GetExistingEpisodes(library, season);
List<TEtag> existingEpisodes = await televisionRepository.GetExistingEpisodes(library, season, cancellationToken);
await foreach ((TEpisode incoming, int _) in episodeEntries.WithCancellation(cancellationToken))
{
@ -413,7 +413,8 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -413,7 +413,8 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
existingEpisodes,
incoming,
localPath,
deepScan))
deepScan,
cancellationToken))
{
continue;
}
@ -485,11 +486,11 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -485,11 +486,11 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
foreach (MediaItemScanResult<TEpisode> result in maybeEpisode.RightToSeq())
{
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming));
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming), cancellationToken);
if (_localFileSystem.FileExists(result.LocalPath))
{
Option<int> flagResult = await televisionRepository.FlagNormal(library, result.Item);
Option<int> flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken);
if (flagResult.IsSome)
{
result.IsUpdated = true;
@ -497,7 +498,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -497,7 +498,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
}
else if (ServerSupportsRemoteStreaming)
{
Option<int> flagResult = await televisionRepository.FlagRemoteOnly(library, result.Item);
Option<int> flagResult = await televisionRepository.FlagRemoteOnly(library, result.Item, cancellationToken);
if (flagResult.IsSome)
{
result.IsUpdated = true;
@ -505,7 +506,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -505,7 +506,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
}
else
{
Option<int> flagResult = await televisionRepository.FlagUnavailable(library, result.Item);
Option<int> flagResult = await televisionRepository.FlagUnavailable(library, result.Item, cancellationToken);
if (flagResult.IsSome)
{
result.IsUpdated = true;
@ -528,7 +529,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -528,7 +529,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
// trash episodes that are no longer present on the media server
var fileNotFoundItemIds = existingEpisodes.Map(m => m.MediaServerItemId).Except(incomingItemIds).ToList();
List<int> ids = await televisionRepository.FlagFileNotFoundEpisodes(library, fileNotFoundItemIds);
List<int> ids = await televisionRepository.FlagFileNotFoundEpisodes(library, fileNotFoundItemIds, cancellationToken);
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty<int>()),
cancellationToken);
@ -544,7 +545,8 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -544,7 +545,8 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
List<TEtag> existingEpisodes,
TEpisode incoming,
string localPath,
bool deepScan)
bool deepScan,
CancellationToken cancellationToken)
{
// deep scan will always pull every episode
if (deepScan)
@ -575,10 +577,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -575,10 +577,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{
if (existingState is not MediaItemState.RemoteOnly)
{
foreach (int id in await televisionRepository.FlagRemoteOnly(library, incoming))
foreach (int id in await televisionRepository.FlagRemoteOnly(library, incoming, cancellationToken))
{
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
new ScannerProgressUpdate(library.Id, null, null, [id], []),
CancellationToken.None);
}
}
@ -587,10 +589,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -587,10 +589,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{
if (existingState is not MediaItemState.Unavailable)
{
foreach (int id in await televisionRepository.FlagUnavailable(library, incoming))
foreach (int id in await televisionRepository.FlagUnavailable(library, incoming, cancellationToken))
{
await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
new ScannerProgressUpdate(library.Id, null, null, [id], []),
CancellationToken.None);
}
}

13
ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs

@ -182,7 +182,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -182,7 +182,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
.BindT(video => ParseRemoteStreamDefinition(video, deserializer, cancellationToken))
.BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath))
.BindT(video => UpdateLibraryFolderId(video, knownFolder))
.BindT(UpdateMetadata)
.BindT(video => UpdateMetadata(video, cancellationToken))
//.BindT(video => UpdateThumbnail(video, cancellationToken))
//.BindT(UpdateSubtitles)
.BindT(FlagNormal);
@ -216,7 +216,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -216,7 +216,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
}
}
foreach (string path in await _remoteStreamRepository.FindRemoteStreamPaths(libraryPath))
foreach (string path in await _remoteStreamRepository.FindRemoteStreamPaths(libraryPath, cancellationToken))
{
if (!_localFileSystem.FileExists(path))
{
@ -234,7 +234,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -234,7 +234,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
else if (Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("Removing dot underscore file at {Path}", path);
List<int> remoteStreamIds = await _remoteStreamRepository.DeleteByPath(libraryPath, path);
List<int> remoteStreamIds = await _remoteStreamRepository.DeleteByPath(libraryPath, path, cancellationToken);
await _mediator.Publish(
new ScannerProgressUpdate(
libraryPath.LibraryId,
@ -336,7 +336,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -336,7 +336,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
if (updated)
{
await _remoteStreamRepository.UpdateDefinition(remoteStream);
await _remoteStreamRepository.UpdateDefinition(remoteStream, cancellationToken);
result.IsUpdated = true;
}
@ -350,7 +350,8 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -350,7 +350,8 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
}
private async Task<Either<BaseError, MediaItemScanResult<RemoteStream>>> UpdateMetadata(
MediaItemScanResult<RemoteStream> result)
MediaItemScanResult<RemoteStream> result,
CancellationToken cancellationToken)
{
try
{
@ -371,7 +372,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -371,7 +372,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
remoteStream.RemoteStreamMetadata ??= [];
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Metadata", path);
if (await _localMetadataProvider.RefreshTagMetadata(remoteStream))
if (await _localMetadataProvider.RefreshTagMetadata(remoteStream, cancellationToken))
{
result.IsUpdated = true;
}

4
ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs

@ -58,7 +58,7 @@ public class PlexNetworkScanner( @@ -58,7 +58,7 @@ public class PlexNetworkScanner(
var keepIds = new System.Collections.Generic.HashSet<int>();
await foreach ((PlexShow item, int _) in items)
{
PlexShowAddTagResult result = await plexTelevisionRepository.AddTag(library, item, tag);
PlexShowAddTagResult result = await plexTelevisionRepository.AddTag(library, item, tag, cancellationToken);
foreach (int existing in result.Existing)
{
@ -74,7 +74,7 @@ public class PlexNetworkScanner( @@ -74,7 +74,7 @@ public class PlexNetworkScanner(
cancellationToken.ThrowIfCancellationRequested();
}
List<int> removedIds = await plexTelevisionRepository.RemoveAllTags(library, tag, keepIds);
List<int> removedIds = await plexTelevisionRepository.RemoveAllTags(library, tag, keepIds, cancellationToken);
var changedIds = removedIds.Concat(addedIds).Distinct().ToList();
if (changedIds.Count > 0)

38
ErsatzTV/Pages/Artist.razor

@ -243,7 +243,7 @@ @@ -243,7 +243,7 @@
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int ArtistId { get; set; }
@ -257,24 +257,34 @@ @@ -257,24 +257,34 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData()
protected override async Task OnParametersSetAsync()
{
await Mediator.Send(new GetArtistById(ArtistId), _cts.Token).IfSomeAsync(vm =>
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_artist = vm;
_sortedLanguages = _artist.Languages.OrderBy(ci => ci.EnglishName).ToList();
_sortedGenres = _artist.Genres.OrderBy(g => g).ToList();
_sortedStyles = _artist.Styles.OrderBy(s => s).ToList();
_sortedMoods = _artist.Moods.OrderBy(m => m).ToList();
});
await Mediator.Send(new GetArtistById(ArtistId), token).IfSomeAsync(vm =>
{
_artist = vm;
_sortedLanguages = _artist.Languages.OrderBy(ci => ci.EnglishName).ToList();
_sortedGenres = _artist.Genres.OrderBy(g => g).ToList();
_sortedStyles = _artist.Styles.OrderBy(s => s).ToList();
_sortedMoods = _artist.Moods.OrderBy(m => m).ToList();
});
_musicVideos = await Mediator.Send(new GetMusicVideoCards(ArtistId, 1, 100), _cts.Token);
_musicVideos = await Mediator.Send(new GetMusicVideoCards(ArtistId, 1, 100), token);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task AddToCollection()

72
ErsatzTV/Pages/BlockEditor.razor

@ -314,7 +314,7 @@ else if (_previewItems != null) @@ -314,7 +314,7 @@ else if (_previewItems != null)
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -327,45 +327,55 @@ else if (_previewItems != null) @@ -327,45 +327,55 @@ else if (_previewItems != null)
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadBlockItems();
private async Task LoadBlockItems()
protected override async Task OnParametersSetAsync()
{
Option<BlockViewModel> maybeBlock = await Mediator.Send(new GetBlockById(Id), _cts.Token);
if (maybeBlock.IsNone)
{
NavigationManager.NavigateTo("blocks");
return;
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
foreach (BlockViewModel block in maybeBlock)
try
{
_block = new BlockItemsEditViewModel
Option<BlockViewModel> maybeBlock = await Mediator.Send(new GetBlockById(Id), token);
if (maybeBlock.IsNone)
{
GroupId = block.GroupId,
GroupName = block.GroupName,
Name = block.Name,
Minutes = block.Minutes,
StopScheduling = block.StopScheduling,
Items = []
};
_durationHours = _block.Minutes / 60;
_durationMinutes = _block.Minutes % 60;
}
NavigationManager.NavigateTo("blocks");
return;
}
Option<IEnumerable<BlockItemViewModel>> maybeResults = await Mediator.Send(new GetBlockItems(Id), _cts.Token);
foreach (IEnumerable<BlockItemViewModel> items in maybeResults)
{
_block.Items.AddRange(items.Map(ProjectToEditViewModel));
if (_block.Items.Count == 1)
foreach (BlockViewModel block in maybeBlock)
{
_selectedItem = _block.Items.Head();
_block = new BlockItemsEditViewModel
{
GroupId = block.GroupId,
GroupName = block.GroupName,
Name = block.Name,
Minutes = block.Minutes,
StopScheduling = block.StopScheduling,
Items = []
};
_durationHours = _block.Minutes / 60;
_durationMinutes = _block.Minutes % 60;
}
Option<IEnumerable<BlockItemViewModel>> maybeResults = await Mediator.Send(new GetBlockItems(Id), token);
foreach (IEnumerable<BlockItemViewModel> items in maybeResults)
{
_block.Items.AddRange(items.Map(ProjectToEditViewModel));
if (_block.Items.Count == 1)
{
_selectedItem = _block.Items.Head();
}
}
}
catch (OperationCanceledException)
{
// do nothing
}
}

52
ErsatzTV/Pages/BlockPlayoutEditor.razor

@ -84,7 +84,7 @@ @@ -84,7 +84,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -99,34 +99,46 @@ @@ -99,34 +99,46 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
Option<string> maybeName = await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token);
if (maybeName.IsNone)
{
NavigationManager.NavigateTo("playouts");
return;
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
foreach (string name in maybeName)
try
{
_channelName = name;
}
Option<string> maybeName = await Mediator.Send(new GetChannelNameByPlayoutId(Id), token);
if (maybeName.IsNone)
{
NavigationManager.NavigateTo("playouts");
return;
}
_decoGroups.Clear();
_decoGroups.AddRange(await Mediator.Send(new GetAllDecoGroups(), _cts.Token));
foreach (string name in maybeName)
{
_channelName = name;
}
_decoGroups.Clear();
_decoGroups.AddRange(await Mediator.Send(new GetAllDecoGroups(), token));
Option<DecoViewModel> maybeDefaultDeco = await Mediator.Send(new GetDecoByPlayoutId(Id), _cts.Token);
foreach (DecoViewModel defaultDeco in maybeDefaultDeco)
Option<DecoViewModel> maybeDefaultDeco = await Mediator.Send(new GetDecoByPlayoutId(Id), token);
foreach (DecoViewModel defaultDeco in maybeDefaultDeco)
{
_enableDefaultDeco = true;
_selectedDefaultDecoGroup = _decoGroups.SingleOrDefault(dg => dg.Id == defaultDeco.DecoGroupId);
await UpdateDefaultDecoTemplateGroupItems(_selectedDefaultDecoGroup);
_defaultDeco = defaultDeco;
}
}
catch (OperationCanceledException)
{
_enableDefaultDeco = true;
_selectedDefaultDecoGroup = _decoGroups.SingleOrDefault(dg => dg.Id == defaultDeco.DecoGroupId);
await UpdateDefaultDecoTemplateGroupItems(_selectedDefaultDecoGroup);
_defaultDeco = defaultDeco;
// do nothing
}
}

32
ErsatzTV/Pages/Blocks.razor

@ -84,7 +84,7 @@ @@ -84,7 +84,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly List<TreeItemData<BlockTreeItemViewModel>> _treeItems = [];
private List<BlockGroupViewModel> _blockGroups = [];
private BlockGroupViewModel _selectedBlockGroup;
@ -93,25 +93,31 @@ @@ -93,25 +93,31 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await ReloadBlockTree();
await InvokeAsync(StateHasChanged);
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
private async Task ReloadBlockTree()
{
_blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token);
try
{
_blockGroups = await Mediator.Send(new GetAllBlockGroups(), token);
_treeItems.Clear();
BlockTreeViewModel tree = await Mediator.Send(new GetBlockTree(), _cts.Token);
foreach (BlockTreeBlockGroupViewModel group in tree.Groups)
_treeItems.Clear();
BlockTreeViewModel tree = await Mediator.Send(new GetBlockTree(), token);
foreach (BlockTreeBlockGroupViewModel group in tree.Groups)
{
_treeItems.Add(new TreeItemData<BlockTreeItemViewModel> { Value = new BlockTreeItemViewModel(group) });
}
}
catch (OperationCanceledException)
{
_treeItems.Add(new TreeItemData<BlockTreeItemViewModel> { Value = new BlockTreeItemViewModel(group) });
// do nothing
}
}

207
ErsatzTV/Pages/ChannelEditor.razor

@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
<MudForm Model="@_model" @ref="@_form" Validation="@(_validator.ValidateValue)" ValidationDelay="0" Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-6" OnClick="HandleSubmitAsync" StartIcon="@(IsEdit ? Icons.Material.Filled.Save : Icons.Material.Filled.Add)">@(IsEdit ? "Save Channel" : "Add Channel")</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-6" OnClick="@HandleSubmitAsync" StartIcon="@(IsEdit ? Icons.Material.Filled.Save : Icons.Material.Filled.Add)">@(IsEdit ? "Save Channel" : "Add Channel")</MudButton>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
@ -221,7 +221,7 @@ else @@ -221,7 +221,7 @@ else
<div class="d-flex">
<MudText>Logo</MudText>
</div>
<InputFile id="fileInput" OnChange="UploadLogo" style="display: none;"/>
<InputFile id="fileInput" OnChange="@UploadLogo" style="display: none;"/>
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
@ -276,7 +276,7 @@ else @@ -276,7 +276,7 @@ else
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int? Id { get; set; }
@ -294,112 +294,135 @@ else @@ -294,112 +294,135 @@ else
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await LoadFFmpegProfiles(_cts.Token);
_availableCultures = await Mediator.Send(new GetAllLanguageCodes(), _cts.Token);
await LoadWatermarks(_cts.Token);
await LoadFillerPresets(_cts.Token);
await LoadMusicVideoCreditsTemplates(_cts.Token);
await LoadChannelStreamSelectors(_cts.Token);
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
if (Id.HasValue)
try
{
Option<ChannelViewModel> maybeChannel = await Mediator.Send(new GetChannelById(Id.Value), _cts.Token);
maybeChannel.Match(
channelViewModel =>
{
_model.Id = channelViewModel.Id;
_model.Name = channelViewModel.Name;
_model.Group = channelViewModel.Group;
_model.Categories = channelViewModel.Categories;
_model.Number = channelViewModel.Number;
_model.FFmpegProfileId = channelViewModel.FFmpegProfileId;
var loadingTasks = new List<Task>
{
LoadDataAsync(() => Mediator.Send(new GetAllFFmpegProfiles(), token), r => _ffmpegProfiles = r),
LoadDataAsync(() => Mediator.Send(new GetAllLanguageCodes(), token), r => _availableCultures = r),
LoadDataAsync(() => Mediator.Send(new GetAllWatermarks(), token), r => _watermarks = r),
LoadDataAsync(() => Mediator.Send(new GetAllFillerPresets(), token)
.Map(list => list.Filter(vm => vm.FillerKind == FillerKind.Fallback).ToList()), r => _fillerPresets = r),
LoadDataAsync(() => Mediator.Send(new GetMusicVideoCreditTemplates(), token), r => _musicVideoCreditsTemplates = r),
LoadDataAsync(() => Mediator.Send(new GetChannelStreamSelectors(), token), r => _streamSelectors = r),
};
if (channelViewModel.Logo.IsExternalUrl)
{
_model.ExternalLogoUrl = channelViewModel.Logo.Path;
}
else
await Task.WhenAll(loadingTasks);
if (Id.HasValue)
{
Option<ChannelViewModel> maybeChannel = await Mediator.Send(new GetChannelById(Id.Value), token);
maybeChannel.Match(
channelViewModel =>
{
_model.Logo = channelViewModel.Logo;
}
_model.Id = channelViewModel.Id;
_model.Name = channelViewModel.Name;
_model.Group = channelViewModel.Group;
_model.Categories = channelViewModel.Categories;
_model.Number = channelViewModel.Number;
_model.FFmpegProfileId = channelViewModel.FFmpegProfileId;
_model.PlayoutMode = channelViewModel.PlayoutMode;
_model.StreamingMode = channelViewModel.StreamingMode;
_model.StreamSelectorMode = channelViewModel.StreamSelectorMode;
_model.StreamSelector = channelViewModel.StreamSelector;
_model.PreferredAudioLanguageCode = channelViewModel.PreferredAudioLanguageCode;
_model.PreferredAudioTitle = channelViewModel.PreferredAudioTitle;
_model.WatermarkId = channelViewModel.WatermarkId;
_model.FallbackFillerId = channelViewModel.FallbackFillerId;
_model.PreferredSubtitleLanguageCode = channelViewModel.PreferredSubtitleLanguageCode;
_model.SubtitleMode = channelViewModel.SubtitleMode;
_model.MusicVideoCreditsMode = channelViewModel.MusicVideoCreditsMode;
_model.MusicVideoCreditsTemplate = channelViewModel.MusicVideoCreditsTemplate;
_model.SongVideoMode = channelViewModel.SongVideoMode;
_model.TranscodeMode = channelViewModel.TranscodeMode;
_model.IdleBehavior = channelViewModel.IdleBehavior;
_model.IsEnabled = channelViewModel.IsEnabled;
_model.ShowInEpg = channelViewModel.ShowInEpg;
},
() => NavigationManager.NavigateTo("404"));
if (channelViewModel.Logo.IsExternalUrl)
{
_model.ExternalLogoUrl = channelViewModel.Logo.Path;
}
else
{
_model.Logo = channelViewModel.Logo;
}
_model.PlayoutMode = channelViewModel.PlayoutMode;
_model.StreamingMode = channelViewModel.StreamingMode;
_model.StreamSelectorMode = channelViewModel.StreamSelectorMode;
_model.StreamSelector = channelViewModel.StreamSelector;
_model.PreferredAudioLanguageCode = channelViewModel.PreferredAudioLanguageCode;
_model.PreferredAudioTitle = channelViewModel.PreferredAudioTitle;
_model.WatermarkId = channelViewModel.WatermarkId;
_model.FallbackFillerId = channelViewModel.FallbackFillerId;
_model.PreferredSubtitleLanguageCode = channelViewModel.PreferredSubtitleLanguageCode;
_model.SubtitleMode = channelViewModel.SubtitleMode;
_model.MusicVideoCreditsMode = channelViewModel.MusicVideoCreditsMode;
_model.MusicVideoCreditsTemplate = channelViewModel.MusicVideoCreditsTemplate;
_model.SongVideoMode = channelViewModel.SongVideoMode;
_model.TranscodeMode = channelViewModel.TranscodeMode;
_model.IdleBehavior = channelViewModel.IdleBehavior;
_model.IsEnabled = channelViewModel.IsEnabled;
_model.ShowInEpg = channelViewModel.ShowInEpg;
},
() => NavigationManager.NavigateTo("404"));
}
else
{
FFmpegSettingsViewModel ffmpegSettings = await Mediator.Send(new GetFFmpegSettings(), token);
// TODO: command for new channel
IEnumerable<int> channelNumbers = await Mediator.Send(new GetAllChannels(), token)
.Map(list => list.Map(c => int.TryParse(c.Number.Split(".").Head(), out int result) ? result : 0));
int maxNumber = Optional(channelNumbers).Flatten().DefaultIfEmpty(0).Max();
_model.Number = (maxNumber + 1).ToString();
_model.Name = "New Channel";
_model.Group = "ErsatzTV";
_model.FFmpegProfileId = ffmpegSettings.DefaultFFmpegProfileId;
_model.StreamingMode = StreamingMode.TransportStreamHybrid;
_model.IsEnabled = true;
_model.ShowInEpg = true;
}
}
else
catch (OperationCanceledException)
{
FFmpegSettingsViewModel ffmpegSettings = await Mediator.Send(new GetFFmpegSettings(), _cts.Token);
// TODO: command for new channel
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));
int maxNumber = Optional(channelNumbers).Flatten().DefaultIfEmpty(0).Max();
_model.Number = (maxNumber + 1).ToString();
_model.Name = "New Channel";
_model.Group = "ErsatzTV";
_model.FFmpegProfileId = ffmpegSettings.DefaultFFmpegProfileId;
_model.StreamingMode = StreamingMode.TransportStreamHybrid;
_model.IsEnabled = true;
_model.ShowInEpg = true;
// do nothing
}
}
private bool IsEdit => Id.HasValue;
private async Task LoadFFmpegProfiles(CancellationToken cancellationToken) =>
_ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles(), cancellationToken);
private async Task LoadWatermarks(CancellationToken cancellationToken) =>
_watermarks = await Mediator.Send(new GetAllWatermarks(), cancellationToken);
private async Task LoadFillerPresets(CancellationToken cancellationToken) =>
_fillerPresets = await Mediator.Send(new GetAllFillerPresets(), cancellationToken)
.Map(list => list.Filter(vm => vm.FillerKind == FillerKind.Fallback).ToList());
private async Task LoadMusicVideoCreditsTemplates(CancellationToken cancellationToken) =>
_musicVideoCreditsTemplates = await Mediator.Send(new GetMusicVideoCreditTemplates(), cancellationToken);
private async Task LoadDataAsync<T>(Func<Task<T>> fetch, Action<T> assign)
{
assign(await fetch());
}
private async Task LoadChannelStreamSelectors(CancellationToken cancellationToken) =>
_streamSelectors = await Mediator.Send(new GetChannelStreamSelectors(), cancellationToken);
private bool IsEdit => Id.HasValue;
private async Task HandleSubmitAsync()
{
await _form.Validate();
ValidationResult result = await _validator.ValidateAsync(_model, _cts.Token);
if (result.IsValid)
if (!_form.IsValid)
{
Seq<BaseError> errorMessage = IsEdit ? (await Mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() : (await Mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
return;
}
errorMessage.HeadOrNone().Match(
error =>
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error saving channel: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("channels"));
try
{
var token = _cts?.Token ?? CancellationToken.None;
ValidationResult result = await _validator.ValidateAsync(_model, token);
if (result.IsValid)
{
Seq<BaseError> errorMessage = IsEdit
? (await Mediator.Send(_model.ToUpdate(), token)).LeftToSeq()
: (await Mediator.Send(_model.ToCreate(), token)).LeftToSeq();
errorMessage.HeadOrNone().Match(
error =>
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error saving channel: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("channels"));
}
}
catch (OperationCanceledException)
{
// do nothing
}
}
@ -407,8 +430,10 @@ else @@ -407,8 +430,10 @@ else
{
try
{
var token = _cts?.Token ?? CancellationToken.None;
Either<BaseError, string> maybeCacheFileName =
await Mediator.Send(new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Logo, e.File.ContentType), _cts.Token);
await Mediator.Send(new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Logo, e.File.ContentType), token);
maybeCacheFileName.Match(
relativeFileName =>
{
@ -422,6 +447,10 @@ else @@ -422,6 +447,10 @@ else
Logger.LogError("Unexpected error saving channel logo: {Error}", error.Value);
});
}
catch (OperationCanceledException)
{
// do nothing
}
catch (IOException)
{
Snackbar.Add("Channel logo exceeds maximum allowed file size of 10 MB", Severity.Error);

6
ErsatzTV/Pages/Channels.razor

@ -150,6 +150,7 @@ @@ -150,6 +150,7 @@
try
{
_ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles(), token);
token.ThrowIfCancellationRequested();
_rowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.ChannelsPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
}
@ -245,9 +246,10 @@ @@ -245,9 +246,10 @@
private async Task<TableData<ChannelViewModel>> ServerReload(TableState state, CancellationToken cancellationToken)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.ChannelsPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.ChannelsPageSize, state.PageSize.ToString()), cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
List<ChannelViewModel> channels = await Mediator.Send(new GetAllChannels(), _cts.Token);
List<ChannelViewModel> channels = await Mediator.Send(new GetAllChannels(), cancellationToken);
IOrderedEnumerable<ChannelViewModel> sorted = channels.OrderBy(c => decimal.Parse(c.Number, CultureInfo.InvariantCulture));
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);

34
ErsatzTV/Pages/CollectionEditor.razor

@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -37,24 +37,36 @@ @@ -37,24 +37,36 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
if (IsEdit)
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
Option<MediaCollectionViewModel> maybeCollection = await Mediator.Send(new GetCollectionById(Id), _cts.Token);
maybeCollection.IfSome(collection =>
if (IsEdit)
{
Option<MediaCollectionViewModel> maybeCollection = await Mediator.Send(new GetCollectionById(Id), token);
maybeCollection.IfSome(collection =>
{
_model.Id = collection.Id;
_model.Name = collection.Name;
});
}
else
{
_model.Id = collection.Id;
_model.Name = collection.Name;
});
_model.Name = "New Collection";
}
}
else
catch (OperationCanceledException)
{
_model.Name = "New Collection";
// do nothing
}
}

42
ErsatzTV/Pages/Collections.razor

@ -153,7 +153,7 @@ @@ -153,7 +153,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private MudTable<MediaCollectionViewModel> _collectionsTable;
private MudTable<MultiCollectionViewModel> _multiCollectionsTable;
@ -165,20 +165,32 @@ @@ -165,20 +165,32 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_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));
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_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));
try
{
_collectionsRowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.CollectionsPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_multiCollectionsRowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.MultiCollectionsPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_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));
_smartCollectionsRowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.SmartCollectionsPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task DeleteMediaCollection(MediaCollectionViewModel collection)
@ -234,25 +246,25 @@ @@ -234,25 +246,25 @@
private async Task<TableData<MediaCollectionViewModel>> ServerReloadCollections(TableState state, CancellationToken cancellationToken)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.CollectionsPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.CollectionsPageSize, state.PageSize.ToString()), cancellationToken);
PagedMediaCollectionsViewModel data = await Mediator.Send(new GetPagedCollections(state.Page, state.PageSize), _cts.Token);
PagedMediaCollectionsViewModel data = await Mediator.Send(new GetPagedCollections(state.Page, state.PageSize), cancellationToken);
return new TableData<MediaCollectionViewModel> { TotalItems = data.TotalCount, Items = data.Page };
}
private async Task<TableData<MultiCollectionViewModel>> ServerReloadMultiCollections(TableState state, CancellationToken cancellationToken)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.MultiCollectionsPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.MultiCollectionsPageSize, state.PageSize.ToString()), cancellationToken);
PagedMultiCollectionsViewModel data = await Mediator.Send(new GetPagedMultiCollections(state.Page, state.PageSize), _cts.Token);
PagedMultiCollectionsViewModel data = await Mediator.Send(new GetPagedMultiCollections(state.Page, state.PageSize), cancellationToken);
return new TableData<MultiCollectionViewModel> { TotalItems = data.TotalCount, Items = data.Page };
}
private async Task<TableData<SmartCollectionViewModel>> ServerReloadSmartCollections(TableState state, CancellationToken cancellationToken)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SmartCollectionsPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SmartCollectionsPageSize, state.PageSize.ToString()), cancellationToken);
PagedSmartCollectionsViewModel data = await Mediator.Send(new GetPagedSmartCollections(state.Page, state.PageSize), _cts.Token);
PagedSmartCollectionsViewModel data = await Mediator.Send(new GetPagedSmartCollections(state.Page, state.PageSize), cancellationToken);
return new TableData<SmartCollectionViewModel> { TotalItems = data.TotalCount, Items = data.Page };
}

144
ErsatzTV/Pages/DecoEditor.razor

@ -336,7 +336,7 @@ @@ -336,7 +336,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -355,86 +355,90 @@ @@ -355,86 +355,90 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await LoadWatermarks();
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_mediaCollections = await Mediator.Send(new GetAllCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_multiCollections = await Mediator.Send(new GetAllMultiCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_smartCollections = await Mediator.Send(new GetAllSmartCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_televisionShows = await Mediator.Send(new GetAllTelevisionShows(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_televisionSeasons = await Mediator.Send(new GetAllTelevisionSeasons(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_artists = await Mediator.Send(new GetAllArtists(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
await LoadDeco();
}
try
{
_watermarks = await Mediator.Send(new GetAllWatermarks(), token);
private async Task LoadWatermarks() =>
_watermarks = await Mediator.Send(new GetAllWatermarks(), _cts.Token);
_mediaCollections = await Mediator.Send(new GetAllCollections(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_multiCollections = await Mediator.Send(new GetAllMultiCollections(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_smartCollections = await Mediator.Send(new GetAllSmartCollections(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_televisionShows = await Mediator.Send(new GetAllTelevisionShows(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_televisionSeasons = await Mediator.Send(new GetAllTelevisionSeasons(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_artists = await Mediator.Send(new GetAllArtists(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
private async Task LoadDeco()
{
Option<DecoViewModel> maybeDeco = await Mediator.Send(new GetDecoById(Id), _cts.Token);
if (maybeDeco.IsNone)
{
NavigationManager.NavigateTo("decos");
return;
}
Option<DecoViewModel> maybeDeco = await Mediator.Send(new GetDecoById(Id), token);
if (maybeDeco.IsNone)
{
NavigationManager.NavigateTo("decos");
return;
}
foreach (DecoViewModel deco in maybeDeco)
{
_deco = new DecoEditViewModel
foreach (DecoViewModel deco in maybeDeco)
{
Name = deco.Name,
DecoGroupId = deco.DecoGroupId,
GroupName = deco.DecoGroupName,
WatermarkMode = deco.WatermarkMode,
Watermarks = deco.Watermarks,
UseWatermarkDuringFiller = deco.UseWatermarkDuringFiller,
_deco = new DecoEditViewModel
{
Name = deco.Name,
DecoGroupId = deco.DecoGroupId,
GroupName = deco.DecoGroupName,
WatermarkMode = deco.WatermarkMode,
Watermarks = deco.Watermarks,
UseWatermarkDuringFiller = deco.UseWatermarkDuringFiller,
DefaultFillerMode = deco.DefaultFillerMode,
DefaultFillerCollectionType = deco.DefaultFillerCollectionType,
DefaultFillerCollection = deco.DefaultFillerCollectionId.HasValue
? _mediaCollections.Find(c => c.Id == deco.DefaultFillerCollectionId!.Value)
: null,
DefaultFillerMediaItem = deco.DefaultFillerMediaItemId.HasValue
? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList()
.Find(vm => vm.MediaItemId == deco.DefaultFillerMediaItemId!.Value)
: null,
DefaultFillerMultiCollection = deco.DefaultFillerMultiCollectionId.HasValue
? _multiCollections.Find(c => c.Id == deco.DefaultFillerMultiCollectionId!.Value)
: null,
DefaultFillerSmartCollection = deco.DefaultFillerSmartCollectionId.HasValue
? _smartCollections.Find(c => c.Id == deco.DefaultFillerSmartCollectionId!.Value)
: null,
DefaultFillerTrimToFit = deco.DefaultFillerTrimToFit,
DefaultFillerMode = deco.DefaultFillerMode,
DefaultFillerCollectionType = deco.DefaultFillerCollectionType,
DefaultFillerCollection = deco.DefaultFillerCollectionId.HasValue
? _mediaCollections.Find(c => c.Id == deco.DefaultFillerCollectionId!.Value)
: null,
DefaultFillerMediaItem = deco.DefaultFillerMediaItemId.HasValue
? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList()
.Find(vm => vm.MediaItemId == deco.DefaultFillerMediaItemId!.Value)
: null,
DefaultFillerMultiCollection = deco.DefaultFillerMultiCollectionId.HasValue
? _multiCollections.Find(c => c.Id == deco.DefaultFillerMultiCollectionId!.Value)
: null,
DefaultFillerSmartCollection = deco.DefaultFillerSmartCollectionId.HasValue
? _smartCollections.Find(c => c.Id == deco.DefaultFillerSmartCollectionId!.Value)
: null,
DefaultFillerTrimToFit = deco.DefaultFillerTrimToFit,
DeadAirFallbackMode = deco.DeadAirFallbackMode,
DeadAirFallbackCollectionType = deco.DeadAirFallbackCollectionType,
DeadAirFallbackCollection = deco.DeadAirFallbackCollectionId.HasValue
? _mediaCollections.Find(c => c.Id == deco.DeadAirFallbackCollectionId!.Value)
: null,
DeadAirFallbackMediaItem = deco.DeadAirFallbackMediaItemId.HasValue
? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList()
.Find(vm => vm.MediaItemId == deco.DeadAirFallbackMediaItemId!.Value)
: null,
DeadAirFallbackMultiCollection = deco.DeadAirFallbackMultiCollectionId.HasValue
? _multiCollections.Find(c => c.Id == deco.DeadAirFallbackMultiCollectionId!.Value)
: null,
DeadAirFallbackSmartCollection = deco.DeadAirFallbackSmartCollectionId.HasValue
? _smartCollections.Find(c => c.Id == deco.DeadAirFallbackSmartCollectionId!.Value)
: null
};
DeadAirFallbackMode = deco.DeadAirFallbackMode,
DeadAirFallbackCollectionType = deco.DeadAirFallbackCollectionType,
DeadAirFallbackCollection = deco.DeadAirFallbackCollectionId.HasValue
? _mediaCollections.Find(c => c.Id == deco.DeadAirFallbackCollectionId!.Value)
: null,
DeadAirFallbackMediaItem = deco.DeadAirFallbackMediaItemId.HasValue
? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList()
.Find(vm => vm.MediaItemId == deco.DeadAirFallbackMediaItemId!.Value)
: null,
DeadAirFallbackMultiCollection = deco.DeadAirFallbackMultiCollectionId.HasValue
? _multiCollections.Find(c => c.Id == deco.DeadAirFallbackMultiCollectionId!.Value)
: null,
DeadAirFallbackSmartCollection = deco.DeadAirFallbackSmartCollectionId.HasValue
? _smartCollections.Find(c => c.Id == deco.DeadAirFallbackSmartCollectionId!.Value)
: null
};
}
}
catch (OperationCanceledException)
{
// do nothing
}
}

38
ErsatzTV/Pages/DecoTemplateEditor.razor

@ -138,7 +138,7 @@ @@ -138,7 +138,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly List<DecoGroupViewModel> _decoGroups = [];
private readonly List<DecoViewModel> _decos = [];
private readonly List<DateTime> _startTimes = [];
@ -156,26 +156,38 @@ @@ -156,26 +156,38 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await LoadDecoTemplateItems();
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
DateTime start = DateTime.Today;
_selectedDecoStart = start;
while (start.Date == DateTime.Today.Date)
try
{
_startTimes.Add(start);
start = start.AddMinutes(5);
await LoadDecoTemplateItems(token);
DateTime start = DateTime.Today;
_selectedDecoStart = start;
while (start.Date == DateTime.Today.Date)
{
_startTimes.Add(start);
start = start.AddMinutes(5);
}
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task LoadDecoTemplateItems()
private async Task LoadDecoTemplateItems(CancellationToken cancellationToken)
{
Option<DecoTemplateViewModel> maybeDecoTemplate = await Mediator.Send(new GetDecoTemplateById(Id), _cts.Token);
Option<DecoTemplateViewModel> maybeDecoTemplate = await Mediator.Send(new GetDecoTemplateById(Id), cancellationToken);
if (maybeDecoTemplate.IsNone)
{
NavigationManager.NavigateTo("deco-templates");
@ -192,13 +204,13 @@ @@ -192,13 +204,13 @@
};
}
Option<IEnumerable<DecoTemplateItemViewModel>> maybeResults = await Mediator.Send(new GetDecoTemplateItems(Id), _cts.Token);
Option<IEnumerable<DecoTemplateItemViewModel>> maybeResults = await Mediator.Send(new GetDecoTemplateItems(Id), cancellationToken);
foreach (IEnumerable<DecoTemplateItemViewModel> items in maybeResults)
{
_decoTemplate.Items.AddRange(items.Map(ProjectToEditViewModel));
}
_decoGroups.AddRange(await Mediator.Send(new GetAllDecoGroups(), _cts.Token));
_decoGroups.AddRange(await Mediator.Send(new GetAllDecoGroups(), cancellationToken));
}
private static DecoTemplateItemEditViewModel ProjectToEditViewModel(DecoTemplateItemViewModel item) =>

32
ErsatzTV/Pages/DecoTemplates.razor

@ -79,7 +79,7 @@ @@ -79,7 +79,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly List<TreeItemData<DecoTemplateTreeItemViewModel>> _treeItems = [];
private List<DecoTemplateGroupViewModel> _decoTemplateGroups = [];
private DecoTemplateGroupViewModel _selectedDecoTemplateGroup;
@ -88,25 +88,31 @@ @@ -88,25 +88,31 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await ReloadDecoTemplateTree();
await InvokeAsync(StateHasChanged);
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
private async Task ReloadDecoTemplateTree()
{
_decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token);
try
{
_decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), token);
_treeItems.Clear();
TreeViewModel tree = await Mediator.Send(new GetDecoTemplateTree(), _cts.Token);
foreach (TreeGroupViewModel group in tree.Groups)
_treeItems.Clear();
TreeViewModel tree = await Mediator.Send(new GetDecoTemplateTree(), token);
foreach (TreeGroupViewModel group in tree.Groups)
{
_treeItems.Add(new TreeItemData<DecoTemplateTreeItemViewModel> { Value = new DecoTemplateTreeItemViewModel(group) });
}
}
catch (OperationCanceledException)
{
_treeItems.Add(new TreeItemData<DecoTemplateTreeItemViewModel> { Value = new DecoTemplateTreeItemViewModel(group) });
// do nothing
}
}

32
ErsatzTV/Pages/Decos.razor

@ -79,7 +79,7 @@ @@ -79,7 +79,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly List<TreeItemData<DecoTreeItemViewModel>> _treeItems = [];
private List<DecoGroupViewModel> _decoGroups = [];
private DecoGroupViewModel _selectedDecoGroup;
@ -88,25 +88,31 @@ @@ -88,25 +88,31 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await ReloadDecoTree();
await InvokeAsync(StateHasChanged);
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
private async Task ReloadDecoTree()
{
_decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token);
try
{
_decoGroups = await Mediator.Send(new GetAllDecoGroups(), token);
_treeItems.Clear();
TreeViewModel tree = await Mediator.Send(new GetDecoTree(), _cts.Token);
foreach (TreeGroupViewModel group in tree.Groups)
_treeItems.Clear();
TreeViewModel tree = await Mediator.Send(new GetDecoTree(), token);
foreach (TreeGroupViewModel group in tree.Groups)
{
_treeItems.Add(new TreeItemData<DecoTreeItemViewModel> { Value = new DecoTreeItemViewModel(group) });
}
}
catch (OperationCanceledException)
{
_treeItems.Add(new TreeItemData<DecoTreeItemViewModel> { Value = new DecoTreeItemViewModel(group) });
// do nothing
}
}

17
ErsatzTV/Pages/EmbyMediaSourceEditor.razor

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
@page "/media/sources/emby/edit"
@using ErsatzTV.Application.Emby
@using ErsatzTV.Core.Emby
@implements IDisposable
@inject IMediator Mediator
<RemoteMediaSourceEditor
@ -10,26 +9,18 @@ @@ -10,26 +9,18 @@
SaveSecrets="SaveSecrets"/>
@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, CancellationToken cancellationToken)
{
EmbySecrets secrets = await Mediator.Send(new GetEmbySecrets(), _cts.Token);
EmbySecrets secrets = await Mediator.Send(new GetEmbySecrets(), cancellationToken);
viewModel.Address = secrets.Address;
viewModel.ApiKey = secrets.ApiKey;
return Unit.Default;
}
private async Task<Either<BaseError, Unit>> SaveSecrets(RemoteMediaSourceEditViewModel viewModel)
private async Task<Either<BaseError, Unit>> SaveSecrets(RemoteMediaSourceEditViewModel viewModel, CancellationToken cancellationToken)
{
var secrets = new EmbySecrets { Address = viewModel.Address, ApiKey = viewModel.ApiKey };
return await Mediator.Send(new SaveEmbySecrets(secrets), _cts.Token);
return await Mediator.Send(new SaveEmbySecrets(secrets), cancellationToken);
}
}

16
ErsatzTV/Pages/EmbyMediaSources.razor

@ -2,7 +2,6 @@ @@ -2,7 +2,6 @@
@using ErsatzTV.Application.Emby
@using ErsatzTV.Core.Emby
@using ErsatzTV.Core.Interfaces.Emby
@implements IDisposable
@inject IEmbySecretStore EmbySecretStore
@inject ChannelWriter<IScannerBackgroundServiceRequest> ScannerWorkerChannel
@ -13,19 +12,10 @@ @@ -13,19 +12,10 @@
Name="Emby"
GetAllMediaSourcesCommand="@(new GetAllEmbyMediaSources())"
DisconnectCommand="@(new DisconnectEmby())"
RefreshLibrariesCommand="@(mediaSourceId => RefreshLibraries(mediaSourceId))"
RefreshLibrariesCommand="@RefreshLibraries"
SecretStore="@EmbySecretStore"/>
@code {
private readonly CancellationTokenSource _cts = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private async Task RefreshLibraries(int mediaSourceId) =>
await ScannerWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(mediaSourceId), _cts.Token);
private async Task RefreshLibraries(int mediaSourceId, CancellationToken cancellationToken) =>
await ScannerWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(mediaSourceId), cancellationToken);
}

17
ErsatzTV/Pages/EmbyPathReplacementsEditor.razor

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
@page "/media/sources/emby/{Id:int}/paths"
@using ErsatzTV.Application.Emby
@using ErsatzTV.Application.MediaSources
@implements IDisposable
@inject IMediator Mediator
<RemoteMediaSourcePathReplacementsEditor
@ -12,23 +11,15 @@ @@ -12,23 +11,15 @@
GetPathReplacementsBySourceId="GetPathReplacementsBySourceId"/>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public int Id { get; set; }
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id) =>
Mediator.Send(new GetEmbyMediaSourceById(Id), _cts.Token)
private Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id, CancellationToken cancellationToken) =>
Mediator.Send(new GetEmbyMediaSourceById(Id), cancellationToken)
.MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address));
private Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId) =>
Mediator.Send(new GetEmbyPathReplacementsBySourceId(Id), _cts.Token)
private Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId, CancellationToken cancellationToken) =>
Mediator.Send(new GetEmbyPathReplacementsBySourceId(Id), cancellationToken)
.Map(list => list.Map(ProjectToEditViewModel).ToList());
private RemoteMediaSourcePathReplacementEditViewModel ProjectToEditViewModel(EmbyPathReplacementViewModel item) =>

25
ErsatzTV/Pages/FFmpeg.razor

@ -72,17 +72,32 @@ @@ -72,17 +72,32 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private List<FFmpegProfileViewModel> _ffmpegProfiles = new();
private List<FFmpegProfileViewModel> _ffmpegProfiles = [];
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadFFmpegProfilesAsync(_cts.Token);
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
await LoadFFmpegProfilesAsync(token);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task LoadFFmpegProfilesAsync(CancellationToken cancellationToken) =>
_ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles(), cancellationToken);

138
ErsatzTV/Pages/FillerPresetEditor.razor

@ -254,7 +254,7 @@ @@ -254,7 +254,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -276,79 +276,91 @@ @@ -276,79 +276,91 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_mediaCollections.AddRange(
await Mediator.Send(new GetAllCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_multiCollections.AddRange(
await Mediator.Send(new GetAllMultiCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_smartCollections.AddRange(
await Mediator.Send(new GetAllSmartCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_televisionShows.AddRange(
await Mediator.Send(new GetAllTelevisionShows(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_televisionSeasons.AddRange(
await Mediator.Send(new GetAllTelevisionSeasons(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_artists.AddRange(
await Mediator.Send(new GetAllArtists(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_playlistGroups.AddRange(await Mediator.Send(new GetAllPlaylistGroups(), _cts.Token));
if (IsEdit)
try
{
Option<FillerPresetViewModel> maybeFillerPreset = await Mediator.Send(new GetFillerPresetById(Id), _cts.Token);
maybeFillerPreset.IfSome(fillerPreset =>
_mediaCollections.AddRange(
await Mediator.Send(new GetAllCollections(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_multiCollections.AddRange(
await Mediator.Send(new GetAllMultiCollections(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_smartCollections.AddRange(
await Mediator.Send(new GetAllSmartCollections(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_televisionShows.AddRange(
await Mediator.Send(new GetAllTelevisionShows(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_televisionSeasons.AddRange(
await Mediator.Send(new GetAllTelevisionSeasons(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_artists.AddRange(
await Mediator.Send(new GetAllArtists(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()));
_playlistGroups.AddRange(await Mediator.Send(new GetAllPlaylistGroups(), token));
if (IsEdit)
{
_model.Id = fillerPreset.Id;
_model.Name = fillerPreset.Name;
_model.FillerKind = fillerPreset.FillerKind;
_model.FillerMode = fillerPreset.FillerMode;
_model.Duration = fillerPreset.Duration;
_model.Count = fillerPreset.Count;
_model.PadToNearestMinute = fillerPreset.PadToNearestMinute;
_model.AllowWatermarks = fillerPreset.AllowWatermarks;
_model.CollectionType = fillerPreset.CollectionType;
_model.Collection = fillerPreset.CollectionId.HasValue
? _mediaCollections.Find(c => c.Id == fillerPreset.CollectionId.Value)
: null;
_model.MultiCollection = fillerPreset.MultiCollectionId.HasValue
? _multiCollections.Find(c => c.Id == fillerPreset.MultiCollectionId.Value)
: null;
_model.SmartCollection = fillerPreset.SmartCollectionId.HasValue
? _smartCollections.Find(c => c.Id == fillerPreset.SmartCollectionId.Value)
: null;
_model.MediaItem = fillerPreset.MediaItemId.HasValue
? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList().Find(vm => vm.MediaItemId == fillerPreset.MediaItemId.Value)
: null;
_model.Expression = fillerPreset.Expression;
_model.UseChaptersAsMediaItems = fillerPreset.UseChaptersAsMediaItems;
_model.Playlist = fillerPreset.Playlist;
});
}
else
{
_model.Name = "New Filler Preset";
_model.FillerKind = FillerKind.PreRoll;
_model.FillerMode = FillerMode.Duration;
}
Option<FillerPresetViewModel> maybeFillerPreset = await Mediator.Send(new GetFillerPresetById(Id), token);
maybeFillerPreset.IfSome(fillerPreset =>
{
_model.Id = fillerPreset.Id;
_model.Name = fillerPreset.Name;
_model.FillerKind = fillerPreset.FillerKind;
_model.FillerMode = fillerPreset.FillerMode;
_model.Duration = fillerPreset.Duration;
_model.Count = fillerPreset.Count;
_model.PadToNearestMinute = fillerPreset.PadToNearestMinute;
_model.AllowWatermarks = fillerPreset.AllowWatermarks;
_model.CollectionType = fillerPreset.CollectionType;
_model.Collection = fillerPreset.CollectionId.HasValue
? _mediaCollections.Find(c => c.Id == fillerPreset.CollectionId.Value)
: null;
_model.MultiCollection = fillerPreset.MultiCollectionId.HasValue
? _multiCollections.Find(c => c.Id == fillerPreset.MultiCollectionId.Value)
: null;
_model.SmartCollection = fillerPreset.SmartCollectionId.HasValue
? _smartCollections.Find(c => c.Id == fillerPreset.SmartCollectionId.Value)
: null;
_model.MediaItem = fillerPreset.MediaItemId.HasValue
? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList().Find(vm => vm.MediaItemId == fillerPreset.MediaItemId.Value)
: null;
_model.Expression = fillerPreset.Expression;
_model.UseChaptersAsMediaItems = fillerPreset.UseChaptersAsMediaItems;
_model.Playlist = fillerPreset.Playlist;
});
}
else
{
_model.Name = "New Filler Preset";
_model.FillerKind = FillerKind.PreRoll;
_model.FillerMode = FillerMode.Duration;
}
foreach (int playlistGroupId in Optional(_model.Playlist?.PlaylistGroupId))
{
foreach (PlaylistGroupViewModel group in Optional(_playlistGroups.Find(g => g.Id == playlistGroupId)))
foreach (int playlistGroupId in Optional(_model.Playlist?.PlaylistGroupId))
{
_selectedPlaylistGroup = group;
await UpdatePlaylistGroupItems(group);
foreach (PlaylistGroupViewModel group in Optional(_playlistGroups.Find(g => g.Id == playlistGroupId)))
{
_selectedPlaylistGroup = group;
await UpdatePlaylistGroupItems(group);
}
}
}
catch (OperationCanceledException)
{
// do nothing
}
}
private bool IsEdit => Id != 0;

29
ErsatzTV/Pages/FillerPresets.razor

@ -72,7 +72,7 @@ @@ -72,7 +72,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private MudTable<FillerPresetViewModel> _fillerPresetsTable;
@ -80,12 +80,27 @@ @@ -80,12 +80,27 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() => _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));
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_fillerPresetsRowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.FillerPresetsPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task DeleteFillerPreset(FillerPresetViewModel fillerPreset)
{
@ -106,9 +121,9 @@ @@ -106,9 +121,9 @@
private async Task<TableData<FillerPresetViewModel>> ServerReloadFillerPresets(TableState state, CancellationToken cancellationToken)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.FillerPresetsPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.FillerPresetsPageSize, state.PageSize.ToString()), cancellationToken);
PagedFillerPresetsViewModel data = await Mediator.Send(new GetPagedFillerPresets(state.Page, state.PageSize), _cts.Token);
PagedFillerPresetsViewModel data = await Mediator.Send(new GetPagedFillerPresets(state.Page, state.PageSize), cancellationToken);
return new TableData<FillerPresetViewModel> { TotalItems = data.TotalCount, Items = data.Page };
}

26
ErsatzTV/Pages/ImageBrowser.razor

@ -48,25 +48,31 @@ @@ -48,25 +48,31 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private List<TreeItemData<ImageTreeItemViewModel>> TreeItems { get; set; } = [];
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await ReloadImageFolders();
await InvokeAsync(StateHasChanged);
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
private async Task ReloadImageFolders()
{
List<ImageFolderViewModel> imageFolders = await Mediator.Send(new GetImageFolders(Option<int>.None), _cts.Token);
TreeItems = imageFolders.Map(g => new TreeItemData<ImageTreeItemViewModel> { Value = new ImageTreeItemViewModel(g) }).ToList();
try
{
List<ImageFolderViewModel> imageFolders = await Mediator.Send(new GetImageFolders(Option<int>.None), token);
TreeItems = imageFolders.Map(g => new TreeItemData<ImageTreeItemViewModel> { Value = new ImageTreeItemViewModel(g) }).ToList();
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task<IReadOnlyCollection<TreeItemData<ImageTreeItemViewModel>>> LoadServerData(ImageTreeItemViewModel parentNode)

17
ErsatzTV/Pages/Index.razor

@ -134,7 +134,7 @@ @@ -134,7 +134,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private string _releaseNotes;
@ -149,8 +149,8 @@ @@ -149,8 +149,8 @@
SystemStartup.OnDatabaseReady -= OnStartupProgress;
SystemStartup.OnSearchIndexReady -= OnStartupProgress;
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
private async void OnStartupProgress(object sender, EventArgs e)
@ -167,6 +167,11 @@ @@ -167,6 +167,11 @@
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
if (MemoryCache.TryGetValue("Index.ReleaseNotesHtml", out string releaseNotesHtml))
@ -191,7 +196,7 @@ @@ -191,7 +196,7 @@
gitHubVersion = $"v{gitHubVersion}";
}
maybeNotes = await GitHubApiClient.GetReleaseNotes(gitHubVersion, _cts.Token);
maybeNotes = await GitHubApiClient.GetReleaseNotes(gitHubVersion, token);
foreach (string notes in maybeNotes.RightToSeq())
{
_releaseNotes = notes;
@ -199,7 +204,7 @@ @@ -199,7 +204,7 @@
}
else
{
maybeNotes = await GitHubApiClient.GetLatestReleaseNotes(_cts.Token);
maybeNotes = await GitHubApiClient.GetLatestReleaseNotes(token);
foreach (string notes in maybeNotes.RightToSeq())
{
_releaseNotes = notes;
@ -222,7 +227,7 @@ @@ -222,7 +227,7 @@
private async Task<TableData<HealthCheckResult>> ServerReload(TableState state, CancellationToken cancellationToken)
{
List<HealthCheckResult> healthCheckResults = await Mediator.Send(new GetAllHealthCheckResults(), _cts.Token);
List<HealthCheckResult> healthCheckResults = await Mediator.Send(new GetAllHealthCheckResults(), cancellationToken);
return new TableData<HealthCheckResult>
{

17
ErsatzTV/Pages/JellyfinMediaSourceEditor.razor

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
@page "/media/sources/jellyfin/edit"
@using ErsatzTV.Application.Jellyfin
@using ErsatzTV.Core.Jellyfin
@implements IDisposable
@inject IMediator Mediator
<RemoteMediaSourceEditor
@ -10,26 +9,18 @@ @@ -10,26 +9,18 @@
SaveSecrets="SaveSecrets"/>
@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, CancellationToken cancellationToken)
{
JellyfinSecrets secrets = await Mediator.Send(new GetJellyfinSecrets(), _cts.Token);
JellyfinSecrets secrets = await Mediator.Send(new GetJellyfinSecrets(), cancellationToken);
viewModel.Address = secrets.Address;
viewModel.ApiKey = secrets.ApiKey;
return Unit.Default;
}
private async Task<Either<BaseError, Unit>> SaveSecrets(RemoteMediaSourceEditViewModel viewModel)
private async Task<Either<BaseError, Unit>> SaveSecrets(RemoteMediaSourceEditViewModel viewModel, CancellationToken cancellationToken)
{
var secrets = new JellyfinSecrets { Address = viewModel.Address, ApiKey = viewModel.ApiKey };
return await Mediator.Send(new SaveJellyfinSecrets(secrets), _cts.Token);
return await Mediator.Send(new SaveJellyfinSecrets(secrets), cancellationToken);
}
}

16
ErsatzTV/Pages/JellyfinMediaSources.razor

@ -2,7 +2,6 @@ @@ -2,7 +2,6 @@
@using ErsatzTV.Application.Jellyfin
@using ErsatzTV.Core.Interfaces.Jellyfin
@using ErsatzTV.Core.Jellyfin
@implements IDisposable
@inject IJellyfinSecretStore JellyfinSecretStore
@inject ChannelWriter<IScannerBackgroundServiceRequest> ScannerWorkerChannel
@ -13,19 +12,10 @@ @@ -13,19 +12,10 @@
Name="Jellyfin"
GetAllMediaSourcesCommand="@(new GetAllJellyfinMediaSources())"
DisconnectCommand="@(new DisconnectJellyfin())"
RefreshLibrariesCommand="@(mediaSourceId => RefreshLibraries(mediaSourceId))"
RefreshLibrariesCommand="@RefreshLibraries"
SecretStore="@JellyfinSecretStore"/>
@code {
private readonly CancellationTokenSource _cts = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private async Task RefreshLibraries(int mediaSourceId) =>
await ScannerWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(mediaSourceId), _cts.Token);
private async Task RefreshLibraries(int mediaSourceId, CancellationToken cancellationToken) =>
await ScannerWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(mediaSourceId), cancellationToken);
}

17
ErsatzTV/Pages/JellyfinPathReplacementsEditor.razor

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
@page "/media/sources/jellyfin/{Id:int}/paths"
@using ErsatzTV.Application.Jellyfin
@using ErsatzTV.Application.MediaSources
@implements IDisposable
@inject IMediator Mediator
<RemoteMediaSourcePathReplacementsEditor
@ -12,23 +11,15 @@ @@ -12,23 +11,15 @@
GetPathReplacementsBySourceId="GetPathReplacementsBySourceId"/>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public int Id { get; set; }
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private async Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id) =>
await Mediator.Send(new GetJellyfinMediaSourceById(Id), _cts.Token)
private async Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id, CancellationToken cancellationToken) =>
await Mediator.Send(new GetJellyfinMediaSourceById(Id), cancellationToken)
.MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address));
private async Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId) =>
await Mediator.Send(new GetJellyfinPathReplacementsBySourceId(Id), _cts.Token)
private async Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId, CancellationToken cancellationToken) =>
await Mediator.Send(new GetJellyfinPathReplacementsBySourceId(Id), cancellationToken)
.Map(list => list.Map(ProjectToEditViewModel).ToList());
private RemoteMediaSourcePathReplacementEditViewModel ProjectToEditViewModel(JellyfinPathReplacementViewModel item) =>

57
ErsatzTV/Pages/Libraries.razor

@ -160,7 +160,7 @@ @@ -160,7 +160,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private IList<LibraryViewModel> _libraries = new List<LibraryViewModel>();
private IList<LibraryViewModel> _externalCollections = new List<LibraryViewModel>();
@ -176,38 +176,51 @@ @@ -176,38 +176,51 @@
Courier.Subscribe<LibraryScanProgress>(HandleScanProgress);
}
protected override async Task OnParametersSetAsync() => await LoadLibraries(_cts.Token);
private async Task LoadLibraries(CancellationToken cancellationToken)
protected override async Task OnParametersSetAsync()
{
_libraries = await Mediator.Send(new GetConfiguredLibraries(), cancellationToken);
_showServerNames = _libraries.Any(l => l is PlexLibraryViewModel);
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_externalCollections = await Mediator.Send(new GetExternalCollections(), cancellationToken);
_progressByLibrary = _libraries.ToDictionary(vm => vm.Id, _ => 0);
try
{
_libraries = await Mediator.Send(new GetConfiguredLibraries(), token);
_showServerNames = _libraries.Any(l => l is PlexLibraryViewModel);
token.ThrowIfCancellationRequested();
_externalCollections = await Mediator.Send(new GetExternalCollections(), token);
_progressByLibrary = _libraries.ToDictionary(vm => vm.Id, _ => 0);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task ScanLibrary(LibraryViewModel library, bool deepScan = false)
{
if (Locker.LockLibrary(library.Id))
{
var token = _cts?.Token ?? CancellationToken.None;
switch (library)
{
case LocalLibraryViewModel:
await ScannerWorkerChannel.WriteAsync(new ForceScanLocalLibrary(library.Id), _cts.Token);
await ScannerWorkerChannel.WriteAsync(new ForceScanLocalLibrary(library.Id), token);
break;
case PlexLibraryViewModel:
await ScannerWorkerChannel.WriteAsync(new SynchronizePlexLibraries(library.MediaSourceId), _cts.Token);
await ScannerWorkerChannel.WriteAsync(new ForceSynchronizePlexLibraryById(library.Id, deepScan), _cts.Token);
await ScannerWorkerChannel.WriteAsync(new SynchronizePlexNetworks(library.Id, true), _cts.Token);
await ScannerWorkerChannel.WriteAsync(new SynchronizePlexLibraries(library.MediaSourceId), token);
await ScannerWorkerChannel.WriteAsync(new ForceSynchronizePlexLibraryById(library.Id, deepScan), token);
await ScannerWorkerChannel.WriteAsync(new SynchronizePlexNetworks(library.Id, true), token);
break;
case JellyfinLibraryViewModel:
await ScannerWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(library.MediaSourceId), _cts.Token);
await ScannerWorkerChannel.WriteAsync(new ForceSynchronizeJellyfinLibraryById(library.Id, deepScan), _cts.Token);
await ScannerWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(library.MediaSourceId), token);
await ScannerWorkerChannel.WriteAsync(new ForceSynchronizeJellyfinLibraryById(library.Id, deepScan), token);
break;
case EmbyLibraryViewModel:
await ScannerWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(library.MediaSourceId), _cts.Token);
await ScannerWorkerChannel.WriteAsync(new ForceSynchronizeEmbyLibraryById(library.Id, deepScan), _cts.Token);
await ScannerWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(library.MediaSourceId), token);
await ScannerWorkerChannel.WriteAsync(new ForceSynchronizeEmbyLibraryById(library.Id, deepScan), token);
break;
}
@ -217,26 +230,27 @@ @@ -217,26 +230,27 @@
private async Task ScanExternalCollections(LibraryViewModel library)
{
var token = _cts?.Token ?? CancellationToken.None;
switch (library.LibraryKind.ToLowerInvariant())
{
case "emby":
if (Locker.LockEmbyCollections())
{
await ScannerWorkerChannel.WriteAsync(new SynchronizeEmbyCollections(library.MediaSourceId, true));
await ScannerWorkerChannel.WriteAsync(new SynchronizeEmbyCollections(library.MediaSourceId, true), token);
}
break;
case "jellyfin":
if (Locker.LockJellyfinCollections())
{
await ScannerWorkerChannel.WriteAsync(new SynchronizeJellyfinCollections(library.MediaSourceId, true));
await ScannerWorkerChannel.WriteAsync(new SynchronizeJellyfinCollections(library.MediaSourceId, true), token);
}
break;
case "plex":
if (Locker.LockPlexCollections())
{
await ScannerWorkerChannel.WriteAsync(new SynchronizePlexCollections(library.MediaSourceId, true));
await ScannerWorkerChannel.WriteAsync(new SynchronizePlexCollections(library.MediaSourceId, true), token);
}
break;
@ -285,8 +299,7 @@ @@ -285,8 +299,7 @@
Locker.OnPlexCollectionsChanged -= LockChanged;
Courier.UnSubscribe<LibraryScanProgress>(HandleScanProgress);
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
}

58
ErsatzTV/Pages/LocalLibraryEditor.razor

@ -84,7 +84,7 @@ @@ -84,7 +84,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -98,30 +98,42 @@ @@ -98,30 +98,42 @@
protected override async Task OnParametersSetAsync()
{
if (IsEdit)
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
Option<LocalLibraryViewModel> maybeLibrary = await Mediator.Send(new GetLocalLibraryById(Id), _cts.Token);
await maybeLibrary.Match(
async library =>
{
_model.Id = library.Id;
_model.Name = library.Name;
_model.MediaKind = library.MediaKind;
if (IsEdit)
{
Option<LocalLibraryViewModel> maybeLibrary = await Mediator.Send(new GetLocalLibraryById(Id), token);
await maybeLibrary.Match(
async library =>
{
_model.Id = library.Id;
_model.Name = library.Name;
_model.MediaKind = library.MediaKind;
await LoadLibraryPaths();
},
() =>
{
NavigationManager.NavigateTo("404");
return Task.CompletedTask;
});
await LoadLibraryPaths();
},
() =>
{
NavigationManager.NavigateTo("404");
return Task.CompletedTask;
});
}
else
{
_model.HasChanges = true;
_model.Name = "New Local Library";
_model.MediaKind = LibraryMediaKind.Movies;
_model.Paths = [];
}
}
else
catch (OperationCanceledException)
{
_model.HasChanges = true;
_model.Name = "New Local Library";
_model.MediaKind = LibraryMediaKind.Movies;
_model.Paths = [];
// do nothing
}
}
@ -252,8 +264,8 @@ @@ -252,8 +264,8 @@
{
Locker.OnLibraryChanged -= LockChanged;
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
}

34
ErsatzTV/Pages/Logs.razor

@ -56,7 +56,7 @@ @@ -56,7 +56,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat;
@ -66,17 +66,31 @@ @@ -66,17 +66,31 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
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));
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_rowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.LogsPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task<TableData<LogEntryViewModel>> ServerReload(TableState state, CancellationToken cancellationToken)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.LogsPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.LogsPageSize, state.PageSize.ToString()), cancellationToken);
PagedLogEntriesViewModel data;
@ -91,7 +105,7 @@ @@ -91,7 +105,7 @@
? Option<bool>.None
: state.SortDirection == SortDirection.Descending
},
_cts.Token);
cancellationToken);
break;
case "level":
data = await Mediator.Send(
@ -102,7 +116,7 @@ @@ -102,7 +116,7 @@
? Option<bool>.None
: state.SortDirection == SortDirection.Descending
},
_cts.Token);
cancellationToken);
break;
default:
data = await Mediator.Send(
@ -110,7 +124,7 @@ @@ -110,7 +124,7 @@
{
SortDescending = Option<bool>.None
},
_cts.Token);
cancellationToken);
break;
}

57
ErsatzTV/Pages/Movie.razor

@ -216,7 +216,7 @@ @@ -216,7 +216,7 @@
}
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private PersistingComponentStateSubscription _persistingSubscription;
[Parameter]
@ -235,8 +235,8 @@ @@ -235,8 +235,8 @@
{
_persistingSubscription.Dispose();
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override Task OnInitializedAsync()
@ -248,22 +248,43 @@ @@ -248,22 +248,43 @@
protected override async Task OnParametersSetAsync()
{
if (!ApplicationState.TryTakeFromJson("_movie", out MovieViewModel restored))
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_movie = await RefreshData();
if (!ApplicationState.TryTakeFromJson("_movie", out MovieViewModel restored))
{
Option<MovieViewModel> maybeMovie = await Mediator.Send(new GetMovieById(MovieId), token);
foreach (var movie in maybeMovie)
{
_movie = movie;
}
if (maybeMovie.IsNone)
{
NavigationManager.NavigateTo("404");
}
}
else
{
_movie = restored;
}
_sortedContentRatings = _movie?.ContentRatings.OrderBy(cr => cr).ToList();
_sortedLanguages = _movie?.Languages.OrderBy(l => l).ToList();
_sortedStudios = _movie?.Studios.OrderBy(s => s).ToList();
_sortedGenres = _movie?.Genres.OrderBy(g => g).ToList();
_sortedTags = _movie?.Tags.OrderBy(t => t).ToList();
_sortedDirectors = _movie?.Directors.OrderBy(d => d).ToList();
_sortedWriters = _movie?.Writers.OrderBy(w => w).ToList();
}
else
catch (OperationCanceledException)
{
_movie = restored;
// do nothing
}
_sortedContentRatings = _movie?.ContentRatings.OrderBy(cr => cr).ToList();
_sortedLanguages = _movie?.Languages.OrderBy(l => l).ToList();
_sortedStudios = _movie?.Studios.OrderBy(s => s).ToList();
_sortedGenres = _movie?.Genres.OrderBy(g => g).ToList();
_sortedTags = _movie?.Tags.OrderBy(t => t).ToList();
_sortedDirectors = _movie?.Directors.OrderBy(d => d).ToList();
_sortedWriters = _movie?.Writers.OrderBy(w => w).ToList();
}
private Task PersistData()
@ -273,12 +294,6 @@ @@ -273,12 +294,6 @@
return Task.CompletedTask;
}
private async Task<MovieViewModel> RefreshData()
{
Option<MovieViewModel> vm = await Mediator.Send(new GetMovieById(MovieId), _cts.Token);
return vm.IsSome ? vm.ValueUnsafe() : null;
}
private async Task AddToCollection()
{
var parameters = new DialogParameters { { "EntityType", "movie" }, { "EntityName", _movie.Title } };

72
ErsatzTV/Pages/MultiCollectionEditor.razor

@ -111,7 +111,7 @@ @@ -111,7 +111,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -130,47 +130,59 @@ @@ -130,47 +130,59 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_collections = await Mediator.Send(new GetAllCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_smartCollections = await Mediator.Send(new GetAllSmartCollections(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
if (IsEdit)
try
{
Option<MultiCollectionViewModel> maybeCollection = await Mediator.Send(new GetMultiCollectionById(Id), _cts.Token);
maybeCollection.IfSome(collection =>
_collections = await Mediator.Send(new GetAllCollections(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_smartCollections = await Mediator.Send(new GetAllSmartCollections(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
if (IsEdit)
{
_model.Id = collection.Id;
_model.Name = collection.Name;
_model.Items = collection.Items
.Map(item =>
new MultiCollectionItemEditViewModel
{
Collection = item.Collection,
ScheduleAsGroup = item.ScheduleAsGroup,
PlaybackOrder = item.PlaybackOrder
})
.Append(
collection.SmartItems.Map(item =>
new MultiCollectionSmartItemEditViewModel
Option<MultiCollectionViewModel> maybeCollection = await Mediator.Send(new GetMultiCollectionById(Id), token);
maybeCollection.IfSome(collection =>
{
_model.Id = collection.Id;
_model.Name = collection.Name;
_model.Items = collection.Items
.Map(item =>
new MultiCollectionItemEditViewModel
{
SmartCollection = item.SmartCollection,
Collection = item.Collection,
ScheduleAsGroup = item.ScheduleAsGroup,
PlaybackOrder = item.PlaybackOrder
})).ToList();
});
})
.Append(
collection.SmartItems.Map(item =>
new MultiCollectionSmartItemEditViewModel
{
SmartCollection = item.SmartCollection,
ScheduleAsGroup = item.ScheduleAsGroup,
PlaybackOrder = item.PlaybackOrder
})).ToList();
});
}
else
{
_model.Name = "New Multi Collection";
_model.Items = new List<MultiCollectionItemEditViewModel>();
}
}
else
catch (OperationCanceledException)
{
_model.Name = "New Multi Collection";
_model.Items = new List<MultiCollectionItemEditViewModel>();
// do nothing
}
}

50
ErsatzTV/Pages/PlaybackTroubleshooting.razor

@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
<div class="d-flex">
<MudText>Media Item ID</MudText>
</div>
<MudTextField T="int?" Value="MediaItemId" ValueChanged="@(async x => await OnMediaItemIdChanged(x))"/>
<MudTextField T="int?" Value="MediaItemId" ValueChanged="@(async x => await OnMediaItemIdChanged(x, CancellationToken.None))"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
@ -137,7 +137,7 @@ @@ -137,7 +137,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly List<FFmpegProfileViewModel> _ffmpegProfiles = [];
private readonly List<WatermarkViewModel> _watermarks = [];
@ -157,8 +157,8 @@ @@ -157,8 +157,8 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override void OnInitialized()
@ -169,23 +169,35 @@ @@ -169,23 +169,35 @@
protected override async Task OnParametersSetAsync()
{
_ffmpegProfiles.Clear();
_ffmpegProfiles.AddRange(await Mediator.Send(new GetAllFFmpegProfiles(), _cts.Token));
if (_ffmpegProfiles.Count > 0)
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_ffmpegProfileId = _ffmpegProfiles.Map(f => f.Id).Head();
}
_ffmpegProfiles.Clear();
_ffmpegProfiles.AddRange(await Mediator.Send(new GetAllFFmpegProfiles(), token));
if (_ffmpegProfiles.Count > 0)
{
_ffmpegProfileId = _ffmpegProfiles.Map(f => f.Id).Head();
}
_watermarks.Clear();
_watermarks.AddRange(await Mediator.Send(new GetAllWatermarks(), _cts.Token));
_watermarks.Clear();
_watermarks.AddRange(await Mediator.Send(new GetAllWatermarks(), token));
await Mediator.Send(new RefreshGraphicsElements(), _cts.Token);
_graphicsElements.Clear();
_graphicsElements.AddRange(await Mediator.Send(new GetAllGraphicsElements(), _cts.Token));
await Mediator.Send(new RefreshGraphicsElements(), token);
_graphicsElements.Clear();
_graphicsElements.AddRange(await Mediator.Send(new GetAllGraphicsElements(), token));
if (MediaItemId is not null)
if (MediaItemId is not null)
{
await OnMediaItemIdChanged(MediaItemId, token);
}
}
catch (OperationCanceledException)
{
await OnMediaItemIdChanged(MediaItemId);
// do nothing
}
}
@ -230,14 +242,14 @@ @@ -230,14 +242,14 @@
_hasPlayed = true;
}
private async Task OnMediaItemIdChanged(int? mediaItemId)
private async Task OnMediaItemIdChanged(int? mediaItemId, CancellationToken cancellationToken)
{
MediaItemId = mediaItemId;
_hasPlayed = false;
foreach (int id in Optional(mediaItemId))
{
Either<BaseError, MediaItemInfo> maybeInfo = await Mediator.Send(new GetMediaItemInfo(id));
Either<BaseError, MediaItemInfo> maybeInfo = await Mediator.Send(new GetMediaItemInfo(id), cancellationToken);
foreach (MediaItemInfo info in maybeInfo.RightToSeq())
{
_info = info;
@ -245,7 +257,7 @@ @@ -245,7 +257,7 @@
_subtitleId = null;
_subtitleStreams.Clear();
_subtitleStreams.AddRange(await Mediator.Send(new GetTroubleshootingSubtitles(id)));
_subtitleStreams.AddRange(await Mediator.Send(new GetTroubleshootingSubtitles(id), cancellationToken));
}
if (maybeInfo.IsLeft)

60
ErsatzTV/Pages/PlaylistEditor.razor

@ -328,7 +328,7 @@ else if (_previewItems is not null) @@ -328,7 +328,7 @@ else if (_previewItems is not null)
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -339,39 +339,49 @@ else if (_previewItems is not null) @@ -339,39 +339,49 @@ else if (_previewItems is not null)
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadPlaylistItems();
private async Task LoadPlaylistItems()
protected override async Task OnParametersSetAsync()
{
Option<PlaylistViewModel> maybePlaylist = await Mediator.Send(new GetPlaylistById(Id), _cts.Token);
if (maybePlaylist.IsNone)
{
NavigationManager.NavigateTo("media/playlists");
return;
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
foreach (PlaylistViewModel playlist in maybePlaylist)
try
{
_playlist = new PlaylistItemsEditViewModel
Option<PlaylistViewModel> maybePlaylist = await Mediator.Send(new GetPlaylistById(Id), token);
if (maybePlaylist.IsNone)
{
Name = playlist.Name,
IsSystem = playlist.IsSystem,
Items = []
};
}
NavigationManager.NavigateTo("media/playlists");
return;
}
Option<IEnumerable<PlaylistItemViewModel>> maybeResults = await Mediator.Send(new GetPlaylistItems(Id), _cts.Token);
foreach (IEnumerable<PlaylistItemViewModel> items in maybeResults)
{
_playlist.Items.AddRange(items.Map(ProjectToEditViewModel));
if (_playlist.Items.Count == 1)
foreach (PlaylistViewModel playlist in maybePlaylist)
{
_selectedItem = _playlist.Items.Head();
_playlist = new PlaylistItemsEditViewModel
{
Name = playlist.Name,
IsSystem = playlist.IsSystem,
Items = []
};
}
Option<IEnumerable<PlaylistItemViewModel>> maybeResults = await Mediator.Send(new GetPlaylistItems(Id), token);
foreach (IEnumerable<PlaylistItemViewModel> items in maybeResults)
{
_playlist.Items.AddRange(items.Map(ProjectToEditViewModel));
if (_playlist.Items.Count == 1)
{
_selectedItem = _playlist.Items.Head();
}
}
}
catch (OperationCanceledException)
{
// do nothing
}
}

30
ErsatzTV/Pages/Playlists.razor

@ -89,7 +89,7 @@ @@ -89,7 +89,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly List<TreeItemData<PlaylistTreeItemViewModel>> _treeItems = [];
private List<PlaylistGroupViewModel> _playlistGroups = [];
private PlaylistGroupViewModel _selectedPlaylistGroup;
@ -98,22 +98,34 @@ @@ -98,22 +98,34 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await ReloadPlaylistTree();
await InvokeAsync(StateHasChanged);
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
await ReloadPlaylistTree(token);
await InvokeAsync(StateHasChanged);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task ReloadPlaylistTree()
private async Task ReloadPlaylistTree(CancellationToken cancellationToken)
{
_playlistGroups = await Mediator.Send(new GetAllPlaylistGroups(), _cts.Token);
_playlistGroups = await Mediator.Send(new GetAllPlaylistGroups(), cancellationToken);
_treeItems.Clear();
TreeViewModel tree = await Mediator.Send(new GetPlaylistTree(), _cts.Token);
TreeViewModel tree = await Mediator.Send(new GetPlaylistTree(), cancellationToken);
foreach (TreeGroupViewModel group in tree.Groups)
{
_treeItems.Add(new TreeItemData<PlaylistTreeItemViewModel> { Value = new PlaylistTreeItemViewModel(group) });
@ -134,7 +146,7 @@ @@ -134,7 +146,7 @@
foreach (PlaylistGroupViewModel _ in result.RightToSeq())
{
await ReloadPlaylistTree();
await ReloadPlaylistTree(_cts.Token);
await InvokeAsync(StateHasChanged);
}
}

36
ErsatzTV/Pages/PlayoutAlternateSchedulesEditor.razor

@ -157,7 +157,7 @@ @@ -157,7 +157,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat;
@ -172,24 +172,34 @@ @@ -172,24 +172,34 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadScheduleItems();
private async Task LoadScheduleItems()
protected override async Task OnParametersSetAsync()
{
_schedules = await Mediator.Send(new GetAllProgramSchedules(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_schedules = await Mediator.Send(new GetAllProgramSchedules(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_channelName = (await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token)).IfNone(string.Empty);
_channelName = (await Mediator.Send(new GetChannelNameByPlayoutId(Id), token)).IfNone(string.Empty);
List<PlayoutAlternateScheduleViewModel> results = await Mediator.Send(new GetPlayoutAlternateSchedules(Id), _cts.Token);
_items = results.Map(ProjectToEditViewModel).ToList();
if (_items.Count == 1)
List<PlayoutAlternateScheduleViewModel> results = await Mediator.Send(new GetPlayoutAlternateSchedules(Id), token);
_items = results.Map(ProjectToEditViewModel).ToList();
if (_items.Count == 1)
{
_selectedItem = _items.Head();
}
}
catch (OperationCanceledException)
{
_selectedItem = _items.Head();
// do nothing
}
}

30
ErsatzTV/Pages/PlayoutEditor.razor

@ -85,7 +85,7 @@ @@ -85,7 +85,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly PlayoutEditViewModel _model = new();
private readonly PlayoutEditViewModelValidator _validator = new();
@ -99,21 +99,33 @@ @@ -99,21 +99,33 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_model.Kind = Kind;
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_channels = await Mediator.Send(new GetAllChannels(), _cts.Token)
.Map(list => list.OrderBy(vm => decimal.Parse(vm.Number, CultureInfo.InvariantCulture)).ToList());
try
{
_model.Kind = Kind;
_channels = await Mediator.Send(new GetAllChannels(), token)
.Map(list => list.OrderBy(vm => decimal.Parse(vm.Number, CultureInfo.InvariantCulture)).ToList());
if (string.IsNullOrWhiteSpace(Kind))
if (string.IsNullOrWhiteSpace(Kind))
{
_programSchedules = await Mediator.Send(new GetAllProgramSchedules(), token)
.Map(list => list.OrderBy(vm => vm.Name).ToList());
}
}
catch (OperationCanceledException)
{
_programSchedules = await Mediator.Send(new GetAllProgramSchedules(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name).ToList());
// do nothing
}
}

40
ErsatzTV/Pages/PlayoutTemplatesEditor.razor

@ -253,7 +253,7 @@ @@ -253,7 +253,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat;
@ -275,27 +275,37 @@ @@ -275,27 +275,37 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadTemplates();
private async Task LoadTemplates()
protected override async Task OnParametersSetAsync()
{
_channelName = (await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token)).IfNone(string.Empty);
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_channelName = (await Mediator.Send(new GetChannelNameByPlayoutId(Id), token)).IfNone(string.Empty);
_templateGroups.Clear();
_templateGroups.AddRange(await Mediator.Send(new GetAllTemplateGroups(), _cts.Token));
_templateGroups.Clear();
_templateGroups.AddRange(await Mediator.Send(new GetAllTemplateGroups(), token));
_decoTemplateGroups.Clear();
_decoTemplateGroups.AddRange(await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token));
_decoTemplateGroups.Clear();
_decoTemplateGroups.AddRange(await Mediator.Send(new GetAllDecoTemplateGroups(), token));
List<PlayoutTemplateViewModel> results = await Mediator.Send(new GetPlayoutTemplates(Id), _cts.Token);
_items = results.Map(ProjectToEditViewModel).ToList();
if (_items.Count == 1)
List<PlayoutTemplateViewModel> results = await Mediator.Send(new GetPlayoutTemplates(Id), token);
_items = results.Map(ProjectToEditViewModel).ToList();
if (_items.Count == 1)
{
await SelectedItemChanged(_items.Head());
}
}
catch (OperationCanceledException)
{
await SelectedItemChanged(_items.Head());
// do nothing
}
}

40
ErsatzTV/Pages/Playouts.razor

@ -248,7 +248,7 @@ @@ -248,7 +248,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat;
@ -281,8 +281,8 @@ @@ -281,8 +281,8 @@
{
Courier.UnSubscribe<PlayoutUpdatedNotification>(HandlePlayoutUpdated);
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
public async Task HandlePlayoutUpdated(PlayoutUpdatedNotification notification, CancellationToken cancellationToken)
@ -301,12 +301,24 @@ @@ -301,12 +301,24 @@
protected override async Task OnParametersSetAsync()
{
_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));
_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));
_showFiller = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller), _cts.Token)
.Map(maybeShow => maybeShow.Match(ce => bool.TryParse(ce.Value, out bool show) && show, () => false));
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_rowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_detailRowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_showFiller = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller), token)
.Map(maybeShow => maybeShow.Match(ce => bool.TryParse(ce.Value, out bool show) && show, () => false));
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task PlayoutSelected(PlayoutNameViewModel playout)
@ -393,9 +405,9 @@ @@ -393,9 +405,9 @@
private async Task<TableData<PlayoutNameViewModel>> ServerReload(TableState state, CancellationToken cancellationToken)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsPageSize, state.PageSize.ToString()), cancellationToken);
List<PlayoutNameViewModel> playouts = await Mediator.Send(new GetAllPlayouts(), _cts.Token);
List<PlayoutNameViewModel> playouts = await Mediator.Send(new GetAllPlayouts(), cancellationToken);
IOrderedEnumerable<PlayoutNameViewModel> sorted = playouts.OrderBy(p => decimal.Parse(p.ChannelNumber, CultureInfo.InvariantCulture));
// TODO: properly page this data
@ -408,13 +420,13 @@ @@ -408,13 +420,13 @@
private async Task<TableData<PlayoutItemViewModel>> DetailServerReload(TableState state, CancellationToken cancellationToken)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller, _showFiller.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize, state.PageSize.ToString()), cancellationToken);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller, _showFiller.ToString()), cancellationToken);
if (_selectedPlayoutId.HasValue)
{
PagedPlayoutItemsViewModel data =
await Mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, _showFiller, state.Page, state.PageSize), _cts.Token);
await Mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, _showFiller, state.Page, state.PageSize), cancellationToken);
return new TableData<PlayoutItemViewModel>
{
TotalItems = data.TotalCount,

17
ErsatzTV/Pages/PlexPathReplacementsEditor.razor

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
@page "/media/sources/plex/{Id:int}/paths"
@using ErsatzTV.Application.MediaSources
@using ErsatzTV.Application.Plex
@implements IDisposable
@inject IMediator Mediator
<RemoteMediaSourcePathReplacementsEditor
@ -12,23 +11,15 @@ @@ -12,23 +11,15 @@
GetPathReplacementsBySourceId="GetPathReplacementsBySourceId"/>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public int Id { get; set; }
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private async Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id) =>
await Mediator.Send(new GetPlexMediaSourceById(Id), _cts.Token)
private async Task<Option<RemoteMediaSourceViewModel>> GetMediaSourceById(int id, CancellationToken cancellationToken) =>
await Mediator.Send(new GetPlexMediaSourceById(Id), cancellationToken)
.MapT(vm => new RemoteMediaSourceViewModel(vm.Id, vm.Name, vm.Address));
private async Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId) =>
await Mediator.Send(new GetPlexPathReplacementsBySourceId(Id), _cts.Token)
private async Task<List<RemoteMediaSourcePathReplacementEditViewModel>> GetPathReplacementsBySourceId(int mediaSourceId, CancellationToken cancellationToken) =>
await Mediator.Send(new GetPlexPathReplacementsBySourceId(Id), cancellationToken)
.Map(list => list.Map(ProjectToEditViewModel).ToList());
private RemoteMediaSourcePathReplacementEditViewModel ProjectToEditViewModel(PlexPathReplacementViewModel item) =>

50
ErsatzTV/Pages/ScheduleEditor.razor

@ -68,7 +68,7 @@ @@ -68,7 +68,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -79,31 +79,43 @@ @@ -79,31 +79,43 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
if (IsEdit)
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
Option<ProgramScheduleViewModel> maybeProgramSchedule = await Mediator.Send(new GetProgramScheduleById(Id), _cts.Token);
maybeProgramSchedule.Match(
viewModel =>
{
_model.Id = viewModel.Id;
_model.Name = viewModel.Name;
_model.ShuffleScheduleItems = viewModel.ShuffleScheduleItems;
_model.KeepMultiPartEpisodesTogether = viewModel.KeepMultiPartEpisodesTogether;
_model.TreatCollectionsAsShows = viewModel.TreatCollectionsAsShows;
_model.RandomStartPoint = viewModel.RandomStartPoint;
_model.FixedStartTimeBehavior = viewModel.FixedStartTimeBehavior;
},
() => NavigationManager.NavigateTo("404"));
if (IsEdit)
{
Option<ProgramScheduleViewModel> maybeProgramSchedule = await Mediator.Send(new GetProgramScheduleById(Id), token);
maybeProgramSchedule.Match(
viewModel =>
{
_model.Id = viewModel.Id;
_model.Name = viewModel.Name;
_model.ShuffleScheduleItems = viewModel.ShuffleScheduleItems;
_model.KeepMultiPartEpisodesTogether = viewModel.KeepMultiPartEpisodesTogether;
_model.TreatCollectionsAsShows = viewModel.TreatCollectionsAsShows;
_model.RandomStartPoint = viewModel.RandomStartPoint;
_model.FixedStartTimeBehavior = viewModel.FixedStartTimeBehavior;
},
() => NavigationManager.NavigateTo("404"));
}
else
{
_model.Name = "New Schedule";
}
}
else
catch (OperationCanceledException)
{
_model.Name = "New Schedule";
// do nothing
}
}

80
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -625,15 +625,15 @@ @@ -625,15 +625,15 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
private ProgramScheduleItemsEditViewModel _schedule = new();
private List<FillerPresetViewModel> _fillerPresets = new();
private List<WatermarkViewModel> _watermarks = new();
private List<LanguageCodeViewModel> _availableCultures = new();
private List<FillerPresetViewModel> _fillerPresets = [];
private List<WatermarkViewModel> _watermarks = [];
private List<LanguageCodeViewModel> _availableCultures = [];
private readonly List<PlaylistGroupViewModel> _playlistGroups = [];
private readonly List<PlaylistViewModel> _playlists = [];
@ -645,47 +645,57 @@ @@ -645,47 +645,57 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadScheduleItems();
private async Task LoadScheduleItems()
protected override async Task OnParametersSetAsync()
{
// TODO: fix performance
_fillerPresets = await Mediator.Send(new GetAllFillerPresets(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_watermarks = await Mediator.Send(new GetAllWatermarks(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_availableCultures = await Mediator.Send(new GetAllLanguageCodes(), _cts.Token);
_playlistGroups.AddRange(await Mediator.Send(new GetAllPlaylistGroups(), _cts.Token));
string name = string.Empty;
var shuffleScheduleItems = false;
Option<ProgramScheduleViewModel> maybeSchedule = await Mediator.Send(new GetProgramScheduleById(Id), _cts.Token);
foreach (ProgramScheduleViewModel schedule in maybeSchedule)
{
name = schedule.Name;
shuffleScheduleItems = schedule.ShuffleScheduleItems;
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
Option<IEnumerable<ProgramScheduleItemViewModel>> maybeResults =
await Mediator.Send(new GetProgramScheduleItems(Id), _cts.Token);
foreach (IEnumerable<ProgramScheduleItemViewModel> items in maybeResults)
try
{
_schedule = new ProgramScheduleItemsEditViewModel
// TODO: fix performance
_fillerPresets = await Mediator.Send(new GetAllFillerPresets(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_watermarks = await Mediator.Send(new GetAllWatermarks(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_availableCultures = await Mediator.Send(new GetAllLanguageCodes(), token);
_playlistGroups.AddRange(await Mediator.Send(new GetAllPlaylistGroups(), token));
string name = string.Empty;
var shuffleScheduleItems = false;
Option<ProgramScheduleViewModel> maybeSchedule = await Mediator.Send(new GetProgramScheduleById(Id), token);
foreach (ProgramScheduleViewModel schedule in maybeSchedule)
{
Name = name,
ShuffleScheduleItems = shuffleScheduleItems,
Items = items.Map(ProjectToEditViewModel).ToList()
};
name = schedule.Name;
shuffleScheduleItems = schedule.ShuffleScheduleItems;
}
if (_schedule.Items.Count == 1)
Option<IEnumerable<ProgramScheduleItemViewModel>> maybeResults =
await Mediator.Send(new GetProgramScheduleItems(Id), token);
foreach (IEnumerable<ProgramScheduleItemViewModel> items in maybeResults)
{
await SelectedItemChanged(_schedule.Items.Head());
_schedule = new ProgramScheduleItemsEditViewModel
{
Name = name,
ShuffleScheduleItems = shuffleScheduleItems,
Items = items.Map(ProjectToEditViewModel).ToList()
};
if (_schedule.Items.Count == 1)
{
await SelectedItemChanged(_schedule.Items.Head());
}
}
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task<IEnumerable<MediaCollectionViewModel>> SearchCollections(string value, CancellationToken cancellationToken)

34
ErsatzTV/Pages/Schedules.razor

@ -103,7 +103,7 @@ @@ -103,7 +103,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private MudTable<ProgramScheduleViewModel> _table;
private MudTable<ProgramScheduleItemViewModel> _detailTable;
@ -113,16 +113,28 @@ @@ -113,16 +113,28 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_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));
_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));
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_rowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.SchedulesPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_detailRowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.SchedulesDetailPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task ScheduleSelected(ProgramScheduleViewModel schedule)
@ -171,9 +183,9 @@ @@ -171,9 +183,9 @@
private async Task<TableData<ProgramScheduleViewModel>> ServerReload(TableState state, CancellationToken cancellationToken)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SchedulesPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SchedulesPageSize, state.PageSize.ToString()), cancellationToken);
List<ProgramScheduleViewModel> schedules = await Mediator.Send(new GetAllProgramSchedules(), _cts.Token);
List<ProgramScheduleViewModel> schedules = await Mediator.Send(new GetAllProgramSchedules(), cancellationToken);
IOrderedEnumerable<ProgramScheduleViewModel> sorted = schedules.OrderBy(s => s.Name, new NaturalSortComparer(StringComparison.CurrentCultureIgnoreCase));
// TODO: properly page this data
@ -186,9 +198,9 @@ @@ -186,9 +198,9 @@
private async Task<TableData<ProgramScheduleItemViewModel>> DetailServerReload(TableState state, CancellationToken cancellationToken)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SchedulesDetailPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SchedulesDetailPageSize, state.PageSize.ToString()), cancellationToken);
List<ProgramScheduleItemViewModel> scheduleItems = await Mediator.Send(new GetProgramScheduleItems(_selectedSchedule.Id), _cts.Token);
List<ProgramScheduleItemViewModel> scheduleItems = await Mediator.Send(new GetProgramScheduleItems(_selectedSchedule.Id), cancellationToken);
IOrderedEnumerable<ProgramScheduleItemViewModel> sorted = scheduleItems.OrderBy(s => s.Index);
// TODO: properly page this data

42
ErsatzTV/Pages/ScriptedPlayoutEditor.razor

@ -40,7 +40,7 @@ @@ -40,7 +40,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private PlayoutNameViewModel _playout;
[Parameter]
@ -50,28 +50,40 @@ @@ -50,28 +50,40 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
Option<string> maybeName = await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token);
if (maybeName.IsNone)
{
NavigationManager.NavigateTo("playouts");
return;
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
foreach (string name in maybeName)
try
{
_channelName = name;
}
Option<string> maybeName = await Mediator.Send(new GetChannelNameByPlayoutId(Id), token);
if (maybeName.IsNone)
{
NavigationManager.NavigateTo("playouts");
return;
}
Option<PlayoutNameViewModel> maybePlayout = await Mediator.Send(new GetPlayoutById(Id), _cts.Token);
foreach (PlayoutNameViewModel playout in maybePlayout)
foreach (string name in maybeName)
{
_channelName = name;
}
Option<PlayoutNameViewModel> maybePlayout = await Mediator.Send(new GetPlayoutById(Id), token);
foreach (PlayoutNameViewModel playout in maybePlayout)
{
_playout = playout;
}
}
catch (OperationCanceledException)
{
_playout = playout;
// do nothing
}
}

42
ErsatzTV/Pages/SequentialPlayoutEditor.razor

@ -40,7 +40,7 @@ @@ -40,7 +40,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private PlayoutNameViewModel _playout;
[Parameter]
@ -50,28 +50,40 @@ @@ -50,28 +50,40 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
Option<string> maybeName = await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token);
if (maybeName.IsNone)
{
NavigationManager.NavigateTo("playouts");
return;
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
foreach (string name in maybeName)
try
{
_channelName = name;
}
Option<string> maybeName = await Mediator.Send(new GetChannelNameByPlayoutId(Id), token);
if (maybeName.IsNone)
{
NavigationManager.NavigateTo("playouts");
return;
}
Option<PlayoutNameViewModel> maybePlayout = await Mediator.Send(new GetPlayoutById(Id), _cts.Token);
foreach (PlayoutNameViewModel playout in maybePlayout)
foreach (string name in maybeName)
{
_channelName = name;
}
Option<PlayoutNameViewModel> maybePlayout = await Mediator.Send(new GetPlayoutById(Id), token);
foreach (PlayoutNameViewModel playout in maybePlayout)
{
_playout = playout;
}
}
catch (OperationCanceledException)
{
_playout = playout;
// do nothing
}
}

45
ErsatzTV/Pages/Settings/FFmpegSettings.razor

@ -152,7 +152,7 @@ @@ -152,7 +152,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private MudForm _form;
private bool _success;
@ -165,22 +165,34 @@ @@ -165,22 +165,34 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await LoadFFmpegProfilesAsync();
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_ffmpegSettings = await Mediator.Send(new GetFFmpegSettings(), _cts.Token);
_success = File.Exists(_ffmpegSettings.FFmpegPath) && File.Exists(_ffmpegSettings.FFprobePath);
_availableCultures = await Mediator.Send(new GetAllLanguageCodes(), _cts.Token);
_watermarks = await Mediator.Send(new GetAllWatermarks(), _cts.Token);
_fillerPresets = await Mediator.Send(new GetAllFillerPresets(), _cts.Token)
.Map(list => list.Filter(fp => fp.FillerKind == FillerKind.Fallback).ToList());
try
{
_ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles(), token);
_ffmpegSettings = await Mediator.Send(new GetFFmpegSettings(), token);
_success = File.Exists(_ffmpegSettings.FFmpegPath) && File.Exists(_ffmpegSettings.FFprobePath);
_availableCultures = await Mediator.Send(new GetAllLanguageCodes(), token);
_watermarks = await Mediator.Send(new GetAllWatermarks(), token);
_fillerPresets = await Mediator.Send(new GetAllFillerPresets(), token)
.Map(list => list.Filter(fp => fp.FillerKind == FillerKind.Fallback).ToList());
await RefreshCustomResolutions();
await RefreshCustomResolutions(token);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private static string ValidatePathExists(string path) => !File.Exists(path) ? "Path does not exist" : null;
@ -191,8 +203,6 @@ @@ -191,8 +203,6 @@
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() =>
_ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles(), _cts.Token);
private async Task SaveFFmpegSettings()
{
@ -214,8 +224,9 @@ @@ -214,8 +224,9 @@
}
}
private async Task RefreshCustomResolutions() => _customResolutions = await Mediator.Send(new GetAllResolutions(), _cts.Token)
.Map(list => list.Filter(r => r.IsCustom).ToList());
private async Task RefreshCustomResolutions(CancellationToken cancellationToken) =>
_customResolutions = await Mediator.Send(new GetAllResolutions(), cancellationToken)
.Map(list => list.Filter(r => r.IsCustom).ToList());
private async Task AddCustomResolution()
{
@ -236,7 +247,7 @@ @@ -236,7 +247,7 @@
if (saveResult.IsNone)
{
await RefreshCustomResolutions();
await RefreshCustomResolutions(_cts.Token);
}
}
}
@ -252,7 +263,7 @@ @@ -252,7 +263,7 @@
if (result.IsNone)
{
await RefreshCustomResolutions();
await RefreshCustomResolutions(_cts.Token);
}
}

24
ErsatzTV/Pages/Settings/HDHRSettings.razor

@ -31,7 +31,7 @@ @@ -31,7 +31,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private MudForm _form;
private bool _hdhrSuccess;
@ -42,15 +42,27 @@ @@ -42,15 +42,27 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_tunerCount = await Mediator.Send(new GetHDHRTunerCount(), _cts.Token);
_uuid = await Mediator.Send(new GetHDHRUUID(), _cts.Token);
_hdhrSuccess = string.IsNullOrWhiteSpace(ValidateTunerCount(_tunerCount));
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_tunerCount = await Mediator.Send(new GetHDHRTunerCount(), token);
_uuid = await Mediator.Send(new GetHDHRUUID(), token);
_hdhrSuccess = string.IsNullOrWhiteSpace(ValidateTunerCount(_tunerCount));
}
catch (OperationCanceledException)
{
// do nothing
}
}
private static string ValidateTunerCount(int tunerCount) => tunerCount <= 0 ? "Tuner count must be greater than zero" : null;

23
ErsatzTV/Pages/Settings/LoggingSettings.razor

@ -85,17 +85,32 @@ @@ -85,17 +85,32 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private LoggingSettingsViewModel _loggingSettings = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() => _loggingSettings = await Mediator.Send(new GetLoggingSettings(), _cts.Token);
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_loggingSettings = await Mediator.Send(new GetLoggingSettings(), token);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task SaveLoggingSettings()
{

22
ErsatzTV/Pages/Settings/PlayoutSettings.razor

@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private MudForm _form;
private bool _playoutSuccess;
@ -40,14 +40,26 @@ @@ -40,14 +40,26 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_playoutSettings = await Mediator.Send(new GetPlayoutSettings(), _cts.Token);
_playoutSuccess = _playoutSettings.DaysToBuild > 0;
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_playoutSettings = await Mediator.Send(new GetPlayoutSettings(), token);
_playoutSuccess = _playoutSettings.DaysToBuild > 0;
}
catch (OperationCanceledException)
{
// do nothing
}
}
private static string ValidatePlayoutDaysToBuild(int daysToBuild) => daysToBuild <= 0 ? "Playout days to build must be greater than zero" : null;

22
ErsatzTV/Pages/Settings/ScannerSettings.razor

@ -24,7 +24,7 @@ @@ -24,7 +24,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private MudForm _form;
private bool _scannerSuccess;
@ -32,14 +32,26 @@ @@ -32,14 +32,26 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_libraryRefreshInterval = await Mediator.Send(new GetLibraryRefreshInterval(), _cts.Token);
_scannerSuccess = _libraryRefreshInterval is >= 0 and < 1_000_000;
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_libraryRefreshInterval = await Mediator.Send(new GetLibraryRefreshInterval(), token);
_scannerSuccess = _libraryRefreshInterval is >= 0 and < 1_000_000;
}
catch (OperationCanceledException)
{
// do nothing
}
}
private static string ValidateLibraryRefreshInterval(int libraryRefreshInterval) => libraryRefreshInterval switch

23
ErsatzTV/Pages/Settings/XMLTVSettings.razor

@ -42,17 +42,32 @@ @@ -42,17 +42,32 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private XmltvSettingsViewModel _xmltvSettings = new();
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() => _xmltvSettings = await Mediator.Send(new GetXmltvSettings(), _cts.Token);
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_xmltvSettings = await Mediator.Send(new GetXmltvSettings(), token);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private static string ValidateXmltvDaysToBuild(int daysToBuild) => daysToBuild <= 0 ? "XMLTV days to build must be greater than zero" : null;

28
ErsatzTV/Pages/SmartCollectionEditor.razor

@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -38,18 +38,30 @@ @@ -38,18 +38,30 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
Option<SmartCollectionViewModel> maybeSmartCollection = await Mediator.Send(new GetSmartCollectionById(Id), _cts.Token);
foreach (SmartCollectionViewModel smartCollection in maybeSmartCollection)
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
Option<SmartCollectionViewModel> maybeSmartCollection = await Mediator.Send(new GetSmartCollectionById(Id), token);
foreach (SmartCollectionViewModel smartCollection in maybeSmartCollection)
{
_model.Id = smartCollection.Id;
_model.Name = smartCollection.Name;
_model.Query = smartCollection.Query;
}
}
catch (OperationCanceledException)
{
_model.Id = smartCollection.Id;
_model.Name = smartCollection.Name;
_model.Query = smartCollection.Query;
// do nothing
}
}

26
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -179,7 +179,7 @@ @@ -179,7 +179,7 @@
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int SeasonId { get; set; }
@ -193,8 +193,8 @@ @@ -193,8 +193,8 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
@ -212,14 +212,22 @@ @@ -212,14 +212,22 @@
}
}
protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData()
protected override async Task OnParametersSetAsync()
{
await Mediator.Send(new GetTelevisionSeasonById(SeasonId), _cts.Token)
.IfSomeAsync(vm => _season = vm);
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_data = await Mediator.Send(new GetTelevisionEpisodeCards(SeasonId, PageNumber, PageSize), _cts.Token);
try
{
await Mediator.Send(new GetTelevisionSeasonById(SeasonId), token).IfSomeAsync(vm => _season = vm);
_data = await Mediator.Send(new GetTelevisionEpisodeCards(SeasonId, PageNumber, PageSize), token);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task AddToCollection()

46
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -176,7 +176,7 @@ @@ -176,7 +176,7 @@
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int ShowId { get; set; }
@ -192,31 +192,41 @@ @@ -192,31 +192,41 @@
private static int PageSize => 100;
private const int PageNumber = 1;
private TelevisionSeasonCardResultsViewModel _data = new(0, new List<TelevisionSeasonCardViewModel>(), null);
private TelevisionSeasonCardResultsViewModel _data = new(0, [], null);
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData()
protected override async Task OnParametersSetAsync()
{
Option<TelevisionShowViewModel> maybeShow = await Mediator.Send(new GetTelevisionShowById(ShowId), _cts.Token);
foreach (TelevisionShowViewModel show in maybeShow)
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_show = show;
_sortedContentRatings = _show.ContentRatings.OrderBy(cr => cr).ToList();
_sortedLanguages = _show.Languages.OrderBy(ci => ci.EnglishName).ToList();
_sortedStudios = _show.Studios.OrderBy(s => s).ToList();
_sortedGenres = _show.Genres.OrderBy(g => g).ToList();
_sortedTags = _show.Tags.OrderBy(t => t).ToList();
_sortedNetworks = _show.Networks.OrderBy(n => n).ToList();
}
Option<TelevisionShowViewModel> maybeShow = await Mediator.Send(new GetTelevisionShowById(ShowId), token);
foreach (TelevisionShowViewModel show in maybeShow)
{
_show = show;
_sortedContentRatings = _show.ContentRatings.OrderBy(cr => cr).ToList();
_sortedLanguages = _show.Languages.OrderBy(ci => ci.EnglishName).ToList();
_sortedStudios = _show.Studios.OrderBy(s => s).ToList();
_sortedGenres = _show.Genres.OrderBy(g => g).ToList();
_sortedTags = _show.Tags.OrderBy(t => t).ToList();
_sortedNetworks = _show.Networks.OrderBy(n => n).ToList();
}
_data = await Mediator.Send(new GetTelevisionSeasonCards(ShowId, PageNumber, PageSize), _cts.Token);
_data = await Mediator.Send(new GetTelevisionSeasonCards(ShowId, PageNumber, PageSize), token);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task AddToCollection()

15
ErsatzTV/Pages/TelevisionShowList.razor

@ -70,13 +70,20 @@ @@ -70,13 +70,20 @@
PageNumber = 1;
}
if (!ApplicationState.TryTakeFromJson("_data", out TelevisionShowCardResultsViewModel restored))
try
{
_data = await RefreshData(CancellationToken);
if (!ApplicationState.TryTakeFromJson("_data", out TelevisionShowCardResultsViewModel restored))
{
_data = await RefreshData(CancellationToken);
}
else
{
_data = restored;
}
}
else
catch (OperationCanceledException)
{
_data = restored;
// do nothing
}
}

38
ErsatzTV/Pages/TemplateEditor.razor

@ -112,7 +112,7 @@ @@ -112,7 +112,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly List<BlockGroupViewModel> _blockGroups = [];
private readonly List<BlockViewModel> _blocks = [];
private readonly List<DateTime> _startTimes = [];
@ -128,26 +128,38 @@ @@ -128,26 +128,38 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await LoadTemplateItems();
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
DateTime start = DateTime.Today;
_selectedBlockStart = start;
while (start.Date == DateTime.Today.Date)
try
{
_startTimes.Add(start);
start = start.AddMinutes(5);
await LoadTemplateItems(token);
DateTime start = DateTime.Today;
_selectedBlockStart = start;
while (start.Date == DateTime.Today.Date)
{
_startTimes.Add(start);
start = start.AddMinutes(5);
}
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task LoadTemplateItems()
private async Task LoadTemplateItems(CancellationToken cancellationToken)
{
Option<TemplateViewModel> maybeTemplate = await Mediator.Send(new GetTemplateById(Id), _cts.Token);
Option<TemplateViewModel> maybeTemplate = await Mediator.Send(new GetTemplateById(Id), cancellationToken);
if (maybeTemplate.IsNone)
{
NavigationManager.NavigateTo("templates");
@ -164,13 +176,13 @@ @@ -164,13 +176,13 @@
};
}
Option<IEnumerable<TemplateItemViewModel>> maybeResults = await Mediator.Send(new GetTemplateItems(Id), _cts.Token);
Option<IEnumerable<TemplateItemViewModel>> maybeResults = await Mediator.Send(new GetTemplateItems(Id), cancellationToken);
foreach (IEnumerable<TemplateItemViewModel> items in maybeResults)
{
_template.Items.AddRange(items.Map(ProjectToEditViewModel));
}
_blockGroups.AddRange(await Mediator.Send(new GetAllBlockGroups(), _cts.Token));
_blockGroups.AddRange(await Mediator.Send(new GetAllBlockGroups(), cancellationToken));
}
private static TemplateItemEditViewModel ProjectToEditViewModel(TemplateItemViewModel item) =>

32
ErsatzTV/Pages/Templates.razor

@ -79,7 +79,7 @@ @@ -79,7 +79,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private readonly List<TreeItemData<TemplateTreeItemViewModel>> _treeItems = [];
private List<TemplateGroupViewModel> _templateGroups = [];
private TemplateGroupViewModel _selectedTemplateGroup;
@ -88,25 +88,31 @@ @@ -88,25 +88,31 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await ReloadTemplateTree();
await InvokeAsync(StateHasChanged);
}
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
private async Task ReloadTemplateTree()
{
_templateGroups = await Mediator.Send(new GetAllTemplateGroups(), _cts.Token);
try
{
_templateGroups = await Mediator.Send(new GetAllTemplateGroups(), token);
_treeItems.Clear();
TreeViewModel tree = await Mediator.Send(new GetTemplateTree(), _cts.Token);
foreach (TreeGroupViewModel group in tree.Groups)
_treeItems.Clear();
TreeViewModel tree = await Mediator.Send(new GetTemplateTree(), token);
foreach (TreeGroupViewModel group in tree.Groups)
{
_treeItems.Add(new TreeItemData<TemplateTreeItemViewModel> { Value = new TemplateTreeItemViewModel(group) });
}
}
catch (OperationCanceledException)
{
_treeItems.Add(new TreeItemData<TemplateTreeItemViewModel> { Value = new TemplateTreeItemViewModel(group) });
// do nothing
}
}

38
ErsatzTV/Pages/TraktListEditor.razor

@ -43,7 +43,7 @@ @@ -43,7 +43,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -54,22 +54,34 @@ @@ -54,22 +54,34 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
Option<TraktListViewModel> maybeTraktList = await Mediator.Send(new GetTraktListById(Id), _cts.Token);
maybeTraktList.Match(
viewModel =>
{
_model.Id = viewModel.Id;
_model.Slug = viewModel.Slug;
_model.AutoRefresh = viewModel.AutoRefresh;
_model.GeneratePlaylist = viewModel.GeneratePlaylist;
},
() => NavigationManager.NavigateTo("404"));
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
Option<TraktListViewModel> maybeTraktList = await Mediator.Send(new GetTraktListById(Id), token);
maybeTraktList.Match(
viewModel =>
{
_model.Id = viewModel.Id;
_model.Slug = viewModel.Slug;
_model.AutoRefresh = viewModel.AutoRefresh;
_model.GeneratePlaylist = viewModel.GeneratePlaylist;
},
() => NavigationManager.NavigateTo("404"));
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task HandleSubmitAsync()

31
ErsatzTV/Pages/TraktLists.razor

@ -83,7 +83,7 @@ @@ -83,7 +83,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private MudTable<TraktListViewModel> _traktListsTable;
@ -91,14 +91,29 @@ @@ -91,14 +91,29 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override void OnInitialized() => Locker.OnTraktChanged += LockChanged;
protected override async Task OnParametersSetAsync() => _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));
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_traktListsRowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.TraktListsPageSize), token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
}
catch (OperationCanceledException)
{
// do nothing
}
}
private void LockChanged(object sender, EventArgs e) =>
InvokeAsync(async () =>
@ -142,9 +157,9 @@ @@ -142,9 +157,9 @@
{
try
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.TraktListsPageSize, state.PageSize.ToString()), _cts.Token);
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.TraktListsPageSize, state.PageSize.ToString()), cancellationToken);
PagedTraktListsViewModel data = await Mediator.Send(new GetPagedTraktLists(state.Page, state.PageSize), _cts.Token);
PagedTraktListsViewModel data = await Mediator.Send(new GetPagedTraktLists(state.Page, state.PageSize), cancellationToken);
return new TableData<TraktListViewModel> { TotalItems = data.TotalCount, Items = data.Page };
}
catch (Exception)
@ -152,7 +167,7 @@ @@ -152,7 +167,7 @@
return new TableData<TraktListViewModel>
{
TotalItems = 0,
Items = Array.Empty<TraktListViewModel>()
Items = []
};
}
}

10
ErsatzTV/Pages/Trash.razor

@ -481,11 +481,11 @@ @@ -481,11 +481,11 @@
LoadDataAsync("_seasons", () => Mediator.Send(new QuerySearchIndexSeasons($"type:season AND ({_query})", 1, 50), token), r => _seasons = r),
LoadDataAsync("_episodes", () => Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50), token), r => _episodes = r),
LoadDataAsync("_musicVideos", () => Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50), token), r => _musicVideos = r),
// LoadDataAsync("_otherVideos", () => Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50), token), r => _otherVideos = r),
// LoadDataAsync("_songs", () => Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50), token), r => _songs = r),
// LoadDataAsync("_images", () => Mediator.Send(new QuerySearchIndexImages($"type:image AND ({_query})", 1, 50), token), r => _images = r),
// LoadDataAsync("_remoteStreams", () => Mediator.Send(new QuerySearchIndexRemoteStreams($"type:remote_stream AND ({_query})", 1, 50), token), r => _remoteStreams = r),
// LoadDataAsync("_artists", () => Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), token), r => _artists = r)
LoadDataAsync("_otherVideos", () => Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50), token), r => _otherVideos = r),
LoadDataAsync("_songs", () => Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50), token), r => _songs = r),
LoadDataAsync("_images", () => Mediator.Send(new QuerySearchIndexImages($"type:image AND ({_query})", 1, 50), token), r => _images = r),
LoadDataAsync("_remoteStreams", () => Mediator.Send(new QuerySearchIndexRemoteStreams($"type:remote_stream AND ({_query})", 1, 50), token), r => _remoteStreams = r),
LoadDataAsync("_artists", () => Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), token), r => _artists = r)
};
await Task.WhenAll(loadingTasks);

17
ErsatzTV/Pages/Troubleshooting.razor

@ -111,7 +111,7 @@ @@ -111,7 +111,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private string _troubleshootingInfo;
private string _nvidiaCapabilities;
private string _qsvCapabilities;
@ -125,15 +125,20 @@ @@ -125,15 +125,20 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
TroubleshootingInfo info = await Mediator.Send(new GetTroubleshootingInfo(), _cts.Token);
TroubleshootingInfo info = await Mediator.Send(new GetTroubleshootingInfo(), token);
_troubleshootingInfo = JsonSerializer.Serialize(
new
@ -159,6 +164,10 @@ @@ -159,6 +164,10 @@
_vaapiCapabilities = info.VaapiCapabilities;
_videoToolboxCapabilities = info.VideoToolboxCapabilities;
}
catch (OperationCanceledException)
{
// do nothing
}
catch (Exception ex)
{
_troubleshootingInfo = ex.ToString();

72
ErsatzTV/Pages/WatermarkEditor.razor

@ -72,7 +72,7 @@ @@ -72,7 +72,7 @@
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{_model.Image.UrlWithContentType}")"/>
</div>
}
else
else if (_model.Image is not null)
{
<MudTextField Disabled="true" For="@(() => _model.Image.Path)"/>
}
@ -203,7 +203,7 @@ @@ -203,7 +203,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -214,18 +214,30 @@ @@ -214,18 +214,30 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
if (IsEdit)
{
Option<WatermarkViewModel> watermark = await Mediator.Send(new GetWatermarkById(Id), _cts.Token);
watermark.Match(
watermarkViewModel => _model = new WatermarkEditViewModel(watermarkViewModel),
() => NavigationManager.NavigateTo("404"));
try
{
Option<WatermarkViewModel> watermark = await Mediator.Send(new GetWatermarkById(Id), token);
watermark.Match(
watermarkViewModel => _model = new WatermarkEditViewModel(watermarkViewModel),
() => NavigationManager.NavigateTo("404"));
}
catch (OperationCanceledException)
{
// do nothing
}
}
else
{
@ -254,18 +266,34 @@ @@ -254,18 +266,34 @@
private async Task HandleSubmitAsync()
{
await _form.Validate();
ValidationResult result = await _validator.ValidateAsync(_model, _cts.Token);
if (result.IsValid)
if (!_form.IsValid)
{
Seq<BaseError> errorMessage = IsEdit ? (await Mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() : (await Mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
return;
}
errorMessage.HeadOrNone().Match(
error =>
{
Snackbar.Add("Unexpected error saving watermark");
Logger.LogError("Unexpected error saving watermark: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("watermarks"));
try
{
var token = _cts?.Token ?? CancellationToken.None;
ValidationResult result = await _validator.ValidateAsync(_model, token);
if (result.IsValid)
{
Seq<BaseError> errorMessage = IsEdit
? (await Mediator.Send(_model.ToUpdate(), token)).LeftToSeq()
: (await Mediator.Send(_model.ToCreate(), token)).LeftToSeq();
errorMessage.HeadOrNone().Match(
error =>
{
Snackbar.Add("Unexpected error saving watermark");
Logger.LogError("Unexpected error saving watermark: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("watermarks"));
}
}
catch (OperationCanceledException)
{
// do nothing
}
}
@ -273,9 +301,11 @@ @@ -273,9 +301,11 @@
{
try
{
var token = _cts?.Token ?? CancellationToken.None;
Either<BaseError, string> maybeCacheFileName = await Mediator.Send(
new SaveArtworkToDisk(e.File.OpenReadStream(SystemEnvironment.MaximumUploadMb * 1024 * 1024), ArtworkKind.Watermark, e.File.ContentType),
_cts.Token);
token);
maybeCacheFileName.Match(
relativeFileName =>
{
@ -288,6 +318,10 @@ @@ -288,6 +318,10 @@
Logger.LogError("Unexpected error saving watermark: {Error}", error.Value);
});
}
catch (OperationCanceledException)
{
// do nothing
}
catch (IOException)
{
Snackbar.Add($"Watermark exceeds maximum allowed file size of {SystemEnvironment.MaximumUploadMb} MB", Severity.Error);

26
ErsatzTV/Pages/Watermarks.razor

@ -76,20 +76,32 @@ @@ -76,20 +76,32 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private List<WatermarkViewModel> _watermarks = [];
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadWatermarksAsync();
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
private async Task LoadWatermarksAsync() =>
_watermarks = await Mediator.Send(new GetAllWatermarks(), _cts.Token);
try
{
_watermarks = await Mediator.Send(new GetAllWatermarks(), token);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task DeleteWatermarkAsync(WatermarkViewModel watermark)
{
@ -101,7 +113,7 @@ @@ -101,7 +113,7 @@
if (result is { Canceled: false })
{
await Mediator.Send(new DeleteWatermark(watermark.Id), _cts.Token);
await LoadWatermarksAsync();
_watermarks = await Mediator.Send(new GetAllWatermarks(), _cts.Token);
}
}

6
ErsatzTV/Pages/YamlValidator.razor

@ -88,15 +88,15 @@ @@ -88,15 +88,15 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
private async Task ValidateYaml()
{
if (_exists)
{
_yamlText = await File.ReadAllTextAsync(_yamlFile);
_yamlText = await File.ReadAllTextAsync(_yamlFile, _cts.Token);
try
{
_jsonText = SequentialScheduleValidator.ToJson(_yamlText);

32
ErsatzTV/Shared/AddToCollectionDialog.razor

@ -38,7 +38,7 @@ @@ -38,7 +38,7 @@
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; }
@ -64,7 +64,7 @@ @@ -64,7 +64,7 @@
private string _newCollectionName;
private List<MediaCollectionViewModel> _collections = new();
private List<MediaCollectionViewModel> _collections = [];
private MediaCollectionViewModel _selectedCollection;
@ -74,8 +74,8 @@ @@ -74,8 +74,8 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
private bool CanSubmit() =>
@ -83,16 +83,28 @@ @@ -83,16 +83,28 @@
protected override async Task OnParametersSetAsync()
{
_collections = await Mediator.Send(new GetAllCollections(), _cts.Token)
.Map(list => new[] { _newCollection }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList());
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
if (MemoryCache.TryGetValue("AddToCollectionDialog.SelectedCollectionId", out int id))
try
{
_selectedCollection = _collections.SingleOrDefault(c => c.Id == id) ?? _newCollection;
_collections = await Mediator.Send(new GetAllCollections(), token)
.Map(list => new[] { _newCollection }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList());
if (MemoryCache.TryGetValue("AddToCollectionDialog.SelectedCollectionId", out int id))
{
_selectedCollection = _collections.SingleOrDefault(c => c.Id == id) ?? _newCollection;
}
else
{
_selectedCollection = _newCollection;
}
}
else
catch (OperationCanceledException)
{
_selectedCollection = _newCollection;
// do nothing
}
}

32
ErsatzTV/Shared/AddToPlaylistDialog.razor

@ -44,7 +44,7 @@ @@ -44,7 +44,7 @@
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; }
@ -73,28 +73,40 @@ @@ -73,28 +73,40 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
private bool CanSubmit() => _selectedPlaylist != null;
protected override async Task OnParametersSetAsync()
{
_playlistGroups.AddRange(await Mediator.Send(new GetAllPlaylistGroups(), _cts.Token));
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
if (MemoryCache.TryGetValue("AddToPlaylistDialog.SelectedPlaylistGroupId", out int groupId))
try
{
_selectedPlaylistGroup = _playlistGroups.SingleOrDefault(pg => pg.Id == groupId);
if (_selectedPlaylistGroup is not null)
_playlistGroups.AddRange(await Mediator.Send(new GetAllPlaylistGroups(), token));
if (MemoryCache.TryGetValue("AddToPlaylistDialog.SelectedPlaylistGroupId", out int groupId))
{
await UpdatePlaylistGroupItems(_selectedPlaylistGroup);
if (MemoryCache.TryGetValue("AddToPlaylistDialog.SelectedPlaylistId", out int id))
_selectedPlaylistGroup = _playlistGroups.SingleOrDefault(pg => pg.Id == groupId);
if (_selectedPlaylistGroup is not null)
{
_selectedPlaylist = _playlists.SingleOrDefault(c => c.Id == id);
await UpdatePlaylistGroupItems(_selectedPlaylistGroup);
if (MemoryCache.TryGetValue("AddToPlaylistDialog.SelectedPlaylistId", out int id))
{
_selectedPlaylist = _playlists.SingleOrDefault(c => c.Id == id);
}
}
}
}
catch (OperationCanceledException)
{
// do nothing
}
}
private string FormatText() => $"Select the playlist to add the {EntityType} {EntityName}";

26
ErsatzTV/Shared/AddToScheduleDialog.razor

@ -29,7 +29,7 @@ @@ -29,7 +29,7 @@
</div>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; }
@ -52,13 +52,27 @@ @@ -52,13 +52,27 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync() =>
_schedules = await Mediator.Send(new GetAllProgramSchedules(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, new NaturalSortComparer(StringComparison.CurrentCultureIgnoreCase)).ToList());
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_schedules = await Mediator.Send(new GetAllProgramSchedules(), token)
.Map(list => list.OrderBy(vm => vm.Name, new NaturalSortComparer(StringComparison.CurrentCultureIgnoreCase)).ToList());
}
catch (OperationCanceledException)
{
// do nothing
}
}
private string FormatText() => $"Select the schedule to add the {EntityType} {EntityName}";

8
ErsatzTV/Shared/CopyFFmpegProfileDialog.razor

@ -19,8 +19,8 @@ @@ -19,8 +19,8 @@
</EditForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit">
<MudButton OnClick="@Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="@Submit">
Copy Profile
</MudButton>
</DialogActions>
@ -43,8 +43,8 @@ @@ -43,8 +43,8 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newFFmpegProfileName);

8
ErsatzTV/Shared/CopyScheduleDialog.razor

@ -19,8 +19,8 @@ @@ -19,8 +19,8 @@
</EditForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit">
<MudButton OnClick="@Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="@Submit">
Copy Schedule
</MudButton>
</DialogActions>
@ -43,8 +43,8 @@ @@ -43,8 +43,8 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newName);

8
ErsatzTV/Shared/CopyWatermarkDialog.razor

@ -19,8 +19,8 @@ @@ -19,8 +19,8 @@
</EditForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit">
<MudButton OnClick="@Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="@Submit">
Copy Watermark
</MudButton>
</DialogActions>
@ -43,8 +43,8 @@ @@ -43,8 +43,8 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newWatermarkName);

16
ErsatzTV/Shared/EditExternalJsonFileDialog.razor

@ -1,6 +1,4 @@ @@ -1,6 +1,4 @@
@implements IDisposable
<MudDialog>
<MudDialog>
<DialogContent>
<MudContainer Class="mb-6">
<MudText>
@ -10,16 +8,14 @@ @@ -10,16 +8,14 @@
<MudTextField Label="External Json File" @bind-Value="_externalJsonFile"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="@(string.IsNullOrWhiteSpace(_externalJsonFile))" OnClick="Submit">
<MudButton OnClick="@Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="@(string.IsNullOrWhiteSpace(_externalJsonFile))" OnClick="@Submit">
Save Changes
</MudButton>
</DialogActions>
</MudDialog>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public string ExternalJsonFile { get; set; }
@ -28,12 +24,6 @@ @@ -28,12 +24,6 @@
private string _externalJsonFile;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override void OnParametersSet() => _externalJsonFile = ExternalJsonFile;
private void Submit() => MudDialog.Close(DialogResult.Ok(_externalJsonFile));

16
ErsatzTV/Shared/EditImageFolderDurationDialog.razor

@ -1,6 +1,4 @@ @@ -1,6 +1,4 @@
@implements IDisposable
<MudDialog>
<MudDialog>
<DialogContent>
<MudContainer Class="mb-6">
<MudText>
@ -15,16 +13,14 @@ @@ -15,16 +13,14 @@
Immediate="true"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit">
<MudButton OnClick="@Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="@Submit">
Save Changes
</MudButton>
</DialogActions>
</MudDialog>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public double? ImageFolderDuration { get; set; }
@ -33,12 +29,6 @@ @@ -33,12 +29,6 @@
private double? _imageDurationSeconds;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override void OnParametersSet() => _imageDurationSeconds = ImageFolderDuration;
private void Submit() => MudDialog.Close(DialogResult.Ok(_imageDurationSeconds));

46
ErsatzTV/Shared/MainLayout.razor

@ -215,7 +215,7 @@ @@ -215,7 +215,7 @@
@code {
private static readonly string InfoVersion = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown";
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
private string _query;
@ -266,8 +266,8 @@ @@ -266,8 +266,8 @@
Courier.UnSubscribe<HealthCheckSummary>(HandleHealthCheckSummary);
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
private static MudTheme ErsatzTvTheme => new()
@ -349,20 +349,42 @@ @@ -349,20 +349,42 @@
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
_query = NavigationManager.Uri.GetSearchQuery();
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
if (SystemStartup.IsDatabaseReady && _searchTargets is null)
try
{
_searchTargets = await Mediator.Send(new QuerySearchTargets(), _cts.Token);
}
await base.OnParametersSetAsync();
_query = NavigationManager.Uri.GetSearchQuery();
if (SystemStartup.IsDatabaseReady && _searchTargets is null)
{
_searchTargets = await Mediator.Send(new QuerySearchTargets(), token);
}
_isDarkMode = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PagesIsDarkMode), _cts.Token)
.MapT(result => !bool.TryParse(result.Value, out bool value) || value)
.IfNoneAsync(true);
_isDarkMode = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PagesIsDarkMode), token)
.MapT(result => !bool.TryParse(result.Value, out bool value) || value)
.IfNoneAsync(true);
}
catch (OperationCanceledException)
{
// do nothing
}
}
protected async void OnSearchTargetsChanged(object sender, EventArgs e) => _searchTargets = await Mediator.Send(new QuerySearchTargets(), _cts.Token);
protected async void OnSearchTargetsChanged(object sender, EventArgs e)
{
try
{
_searchTargets = await Mediator.Send(new QuerySearchTargets(), _cts.Token);
}
catch (Exception)
{
// do nothing
}
}
private void PerformSearch()
{

36
ErsatzTV/Shared/MoveLocalLibraryPathDialog.razor

@ -37,7 +37,7 @@ @@ -37,7 +37,7 @@
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; }
@ -61,8 +61,8 @@ @@ -61,8 +61,8 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
private bool CanSubmit() =>
@ -70,19 +70,31 @@ @@ -70,19 +70,31 @@
protected override async Task OnParametersSetAsync()
{
_newLibrary = new LocalLibraryViewModel(-1, "(New Library)", MediaKind, -1);
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_libraries = await Mediator.Send(new GetAllLocalLibraries(), _cts.Token)
.Map(list => list.Filter(ll => ll.MediaKind == MediaKind && ll.Id != SourceLibraryId))
.Map(list => new[] { _newLibrary }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList());
if (MemoryCache.TryGetValue("MoveLocalLibraryPathDialog.SelectedLibraryId", out int id))
try
{
_selectedLibrary = _libraries.SingleOrDefault(c => c.Id == id) ?? _newLibrary;
_newLibrary = new LocalLibraryViewModel(-1, "(New Library)", MediaKind, -1);
_libraries = await Mediator.Send(new GetAllLocalLibraries(), token)
.Map(list => list.Filter(ll => ll.MediaKind == MediaKind && ll.Id != SourceLibraryId))
.Map(list => new[] { _newLibrary }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList());
if (MemoryCache.TryGetValue("MoveLocalLibraryPathDialog.SelectedLibraryId", out int id))
{
_selectedLibrary = _libraries.SingleOrDefault(c => c.Id == id) ?? _newLibrary;
}
else
{
_selectedLibrary = _newLibrary;
}
}
else
catch (OperationCanceledException)
{
_selectedLibrary = _newLibrary;
// do nothing
}
}

32
ErsatzTV/Shared/RemoteMediaSourceEditor.razor

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
@inject IMediator Mediator
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ISnackbar Snackbar
@inject ILogger<RemoteMediaSourceEditor> Logger
@ -24,21 +24,43 @@ @@ -24,21 +24,43 @@
</MudContainer>
@code {
private CancellationTokenSource _cts;
[Parameter]
public string Name { get; set; }
[Parameter]
public Func<RemoteMediaSourceEditViewModel, Task<Unit>> LoadSecrets { get; set; }
public Func<RemoteMediaSourceEditViewModel, CancellationToken, Task<Unit>> LoadSecrets { get; set; }
[Parameter]
public Func<RemoteMediaSourceEditViewModel, Task<Either<BaseError, Unit>>> SaveSecrets { get; set; }
public Func<RemoteMediaSourceEditViewModel, CancellationToken, Task<Either<BaseError, Unit>>> SaveSecrets { get; set; }
private readonly RemoteMediaSourceEditViewModel _model = new();
private EditContext _editContext;
private ValidationMessageStore _messageStore;
protected override async Task OnParametersSetAsync() => await LoadSecrets(_model);
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
await LoadSecrets(_model, token);
}
catch (OperationCanceledException)
{
// do nothing
}
}
protected override void OnInitialized()
{
@ -51,7 +73,7 @@ @@ -51,7 +73,7 @@
_messageStore.Clear();
if (_editContext.Validate())
{
Either<BaseError, Unit> result = await SaveSecrets(_model);
Either<BaseError, Unit> result = await SaveSecrets(_model, _cts.Token);
result.Match(
_ => NavigationManager.NavigateTo($"media/sources/{Name.ToLowerInvariant()}"),
error =>

4
ErsatzTV/Shared/RemoteMediaSourceLibrariesEditor.razor

@ -77,8 +77,8 @@ @@ -77,8 +77,8 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override Task OnParametersSetAsync() => LoadData();

50
ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor

@ -79,7 +79,7 @@ @@ -79,7 +79,7 @@
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private CancellationTokenSource _cts;
[Parameter]
public int Id { get; set; }
@ -88,10 +88,10 @@ @@ -88,10 +88,10 @@
public string Name { get; set; }
[Parameter]
public Func<int, Task<Option<RemoteMediaSourceViewModel>>> GetMediaSourceById { get; set; }
public Func<int, CancellationToken, Task<Option<RemoteMediaSourceViewModel>>> GetMediaSourceById { get; set; }
[Parameter]
public Func<int, Task<List<RemoteMediaSourcePathReplacementEditViewModel>>> GetPathReplacementsBySourceId { get; set; }
public Func<int, CancellationToken, Task<List<RemoteMediaSourcePathReplacementEditViewModel>>> GetPathReplacementsBySourceId { get; set; }
[Parameter]
public Func<List<RemoteMediaSourcePathReplacementEditViewModel>, IRequest<Either<BaseError, Unit>>> GetUpdatePathReplacementsRequest { get; set; }
@ -103,26 +103,36 @@ @@ -103,26 +103,36 @@
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_cts?.Cancel();
_cts?.Dispose();
}
protected override Task OnParametersSetAsync() => LoadData();
private async Task LoadData()
protected override async Task OnParametersSetAsync()
{
Option<RemoteMediaSourceViewModel> maybeSource = await GetMediaSourceById(Id);
await maybeSource.Match(
async source =>
{
_source = source;
_pathReplacements = await GetPathReplacementsBySourceId(Id);
},
() =>
{
NavigationManager.NavigateTo("404");
return Task.CompletedTask;
});
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
Option<RemoteMediaSourceViewModel> maybeSource = await GetMediaSourceById(Id, token);
await maybeSource.Match(
async source =>
{
_source = source;
_pathReplacements = await GetPathReplacementsBySourceId(Id, token);
},
() =>
{
NavigationManager.NavigateTo("404");
return Task.CompletedTask;
});
}
catch (OperationCanceledException)
{
// do nothing
}
}
private void AddPathReplacement()

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save