diff --git a/ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs b/ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs index f2c094876..f9e6183fc 100644 --- a/ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetAllChannelsForApiHandler.cs @@ -15,7 +15,7 @@ public class GetAllChannelsForApiHandler : IRequestHandler channels = Optional(await _channelRepository.GetAll()).Flatten(); + IEnumerable channels = Optional(await _channelRepository.GetAll(cancellationToken)).Flatten(); return channels.Map(ProjectToResponseModel).ToList(); } } diff --git a/ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs b/ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs index 9c1d28563..52d1872f5 100644 --- a/ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs @@ -3,12 +3,10 @@ using static ErsatzTV.Application.Channels.Mapper; namespace ErsatzTV.Application.Channels; -public class GetAllChannelsHandler : IRequestHandler> +public class GetAllChannelsHandler(IChannelRepository channelRepository) + : IRequestHandler> { - private readonly IChannelRepository _channelRepository; - - public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; - public async Task> Handle(GetAllChannels request, CancellationToken cancellationToken) => - Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList(); + await channelRepository.GetAll(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); } diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelLineupHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelLineupHandler.cs index a268e1ff4..f1d7b314b 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelLineupHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelLineupHandler.cs @@ -10,7 +10,7 @@ public class GetChannelLineupHandler : IRequestHandler _channelRepository = channelRepository; public Task> 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()); } diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs index d4132aa19..bffec02ce 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs @@ -12,7 +12,7 @@ public class GetChannelPlaylistHandler : IRequestHandler Handle(GetChannelPlaylist request, CancellationToken cancellationToken) => - _channelRepository.GetAll() + _channelRepository.GetAll(cancellationToken) .Map(channels => EnsureMode(channels, request.Mode)) .Map(channels => new ChannelPlaylist( request.Scheme, diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs index 8a566c723..18865fb26 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs @@ -4,18 +4,14 @@ using static ErsatzTV.Application.FFmpegProfiles.Mapper; namespace ErsatzTV.Application.FFmpegProfiles; -public class GetAllFFmpegProfilesHandler : IRequestHandler> +public class GetAllFFmpegProfilesHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> { - private readonly IDbContextFactory _dbContextFactory; - - public GetAllFFmpegProfilesHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - public async Task> 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) diff --git a/ErsatzTV.Application/Watermarks/Commands/CopyWatermarkHandler.cs b/ErsatzTV.Application/Watermarks/Commands/CopyWatermarkHandler.cs index 1f4c0157a..be1b4383a 100644 --- a/ErsatzTV.Application/Watermarks/Commands/CopyWatermarkHandler.cs +++ b/ErsatzTV.Application/Watermarks/Commands/CopyWatermarkHandler.cs @@ -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> +public class CopyWatermarkHandler(IDbContextFactory dbContextFactory, ISearchTargets searchTargets) + : IRequestHandler> { - private readonly IDbContextFactory _dbContextFactory; - private readonly ISearchTargets _searchTargets; - - public CopyWatermarkHandler(IDbContextFactory dbContextFactory, ISearchTargets searchTargets) + public async Task> Handle( + CopyWatermark request, + CancellationToken cancellationToken) { - _dbContextFactory = dbContextFactory; - _searchTargets = searchTargets; + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request, cancellationToken); + return await validation.Apply(c => PerformCopy(dbContext, c, cancellationToken)); } - public Task> Handle( - CopyWatermark request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(PerformCopy) - .Bind(v => v.ToEitherAsync()); - - private async Task PerformCopy(CopyWatermark request) + private async Task 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> Validate(CopyWatermark request) => - ValidateName(request).AsTask().MapT(_ => request); + private static async Task> Validate( + TvContext dbContext, + CopyWatermark request, + CancellationToken cancellationToken) => + (ValidateName(request), await WatermarkMustExist(dbContext, request, cancellationToken)) + .Apply((name, watermark) => new CopyWatermarkParameters(name, watermark)); private static Validation ValidateName(CopyWatermark request) => request.NotEmpty(x => x.Name) .Bind(_ => request.NotLongerThan(50)(x => x.Name)); + + private static Task> WatermarkMustExist( + TvContext dbContext, + CopyWatermark copyWatermark, + CancellationToken cancellationToken) => + dbContext.ChannelWatermarks + .SelectOneAsync(wm => wm.Id, wm => wm.Id == copyWatermark.WatermarkId, cancellationToken) + .Map(o => o.ToValidation($"Watermark {copyWatermark.WatermarkId} does not exist.")); + + private sealed record CopyWatermarkParameters(string Name, ChannelWatermark Watermark); } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs index 2b07779e4..65cb708fc 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs @@ -6,6 +6,6 @@ public interface IChannelRepository { Task> GetChannel(int id); Task> GetByNumber(string number); - Task> GetAll(); + Task> GetAll(CancellationToken cancellationToken); Task> GetWatermarkByName(string name); } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs index 428c87234..dbb9e4835 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs @@ -6,7 +6,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories; public interface IEmbyTelevisionRepository : IMediaServerTelevisionRepository { - Task> GetShowTitleItemId(int libraryId, int showId); + Task> GetShowTitleItemId( + int libraryId, + int showId, + CancellationToken cancellationToken); } public record EmbyShowTitleItemIdResult(string Title, string ItemId); diff --git a/ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs index f8bb53ef7..0ec40c866 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs @@ -7,7 +7,10 @@ public interface IJellyfinTelevisionRepository : IMediaServerTelevisionRepositor JellyfinSeason, JellyfinEpisode, JellyfinItemEtag> { - Task> GetShowTitleItemId(int libraryId, int showId); + Task> GetShowTitleItemId( + int libraryId, + int showId, + CancellationToken cancellationToken); } public record JellyfinShowTitleItemIdResult(string Title, string ItemId); diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaServerTelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaServerTelevisionRepository.cs index b57ae9d24..8a8dc9695 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaServerTelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaServerTelevisionRepository.cs @@ -9,9 +9,9 @@ public interface IMediaServerTelevisionRepository> GetExistingShows(TLibrary library); - Task> GetExistingSeasons(TLibrary library, TShow show); - Task> GetExistingEpisodes(TLibrary library, TSeason season); + Task> GetExistingShows(TLibrary library, CancellationToken cancellationToken); + Task> GetExistingSeasons(TLibrary library, TShow show, CancellationToken cancellationToken); + Task> GetExistingEpisodes(TLibrary library, TSeason season, CancellationToken cancellationToken); Task>> GetOrAdd( TLibrary library, @@ -29,15 +29,24 @@ public interface IMediaServerTelevisionRepository SetEtag(TShow show, string etag); - Task SetEtag(TSeason season, string etag); - Task SetEtag(TEpisode episode, string etag); - Task> FlagNormal(TLibrary library, TEpisode episode); - Task> FlagNormal(TLibrary library, TSeason season); - Task> FlagNormal(TLibrary library, TShow show); - Task> FlagFileNotFoundShows(TLibrary library, List showItemIds); - Task> FlagFileNotFoundSeasons(TLibrary library, List seasonItemIds); - Task> FlagFileNotFoundEpisodes(TLibrary library, List episodeItemIds); - Task> FlagUnavailable(TLibrary library, TEpisode episode); - Task> FlagRemoteOnly(TLibrary library, TEpisode episode); + Task SetEtag(TShow show, string etag, CancellationToken cancellationToken); + Task SetEtag(TSeason season, string etag, CancellationToken cancellationToken); + Task SetEtag(TEpisode episode, string etag, CancellationToken cancellationToken); + Task> FlagNormal(TLibrary library, TEpisode episode, CancellationToken cancellationToken); + Task> FlagNormal(TLibrary library, TSeason season, CancellationToken cancellationToken); + Task> FlagNormal(TLibrary library, TShow show, CancellationToken cancellationToken); + Task> FlagFileNotFoundShows( + TLibrary library, + List showItemIds, + CancellationToken cancellationToken); + Task> FlagFileNotFoundSeasons( + TLibrary library, + List seasonItemIds, + CancellationToken cancellationToken); + Task> FlagFileNotFoundEpisodes( + TLibrary library, + List episodeItemIds, + CancellationToken cancellationToken); + Task> FlagUnavailable(TLibrary library, TEpisode episode, CancellationToken cancellationToken); + Task> FlagRemoteOnly(TLibrary library, TEpisode episode, CancellationToken cancellationToken); } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IPlexTelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IPlexTelevisionRepository.cs index 05e60bb2f..8647addc8 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IPlexTelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IPlexTelevisionRepository.cs @@ -6,10 +6,18 @@ namespace ErsatzTV.Core.Interfaces.Repositories; public interface IPlexTelevisionRepository : IMediaServerTelevisionRepository { - Task> RemoveAllTags(PlexLibrary library, PlexTag tag, System.Collections.Generic.HashSet keep); - Task AddTag(PlexLibrary library, PlexShow show, PlexTag tag); - Task UpdateLastNetworksScan(PlexLibrary library); - Task> GetShowTitleKey(int libraryId, int showId); + Task> RemoveAllTags( + PlexLibrary library, + PlexTag tag, + System.Collections.Generic.HashSet keep, + CancellationToken cancellationToken); + Task AddTag( + PlexLibrary library, + PlexShow show, + PlexTag tag, + CancellationToken cancellationToken); + Task UpdateLastNetworksScan(PlexLibrary library, CancellationToken cancellationToken); + Task> GetShowTitleKey(int libraryId, int showId, CancellationToken cancellationToken); } public record PlexShowAddTagResult(Option Existing, Option Added); diff --git a/ErsatzTV.Core/Interfaces/Repositories/IRemoteStreamRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IRemoteStreamRepository.cs index 74d3c4332..4935f72f3 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IRemoteStreamRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IRemoteStreamRepository.cs @@ -11,8 +11,8 @@ public interface IRemoteStreamRepository string path, CancellationToken cancellationToken); - Task> FindRemoteStreamPaths(LibraryPath libraryPath); - Task> DeleteByPath(LibraryPath libraryPath, string path); - Task AddTag(RemoteStreamMetadata metadata, Tag tag); - Task UpdateDefinition(RemoteStream remoteStream); + Task> FindRemoteStreamPaths(LibraryPath libraryPath, CancellationToken cancellationToken); + Task> DeleteByPath(LibraryPath libraryPath, string path, CancellationToken cancellationToken); + Task AddTag(RemoteStreamMetadata metadata, Tag tag, CancellationToken cancellationToken); + Task UpdateDefinition(RemoteStream remoteStream, CancellationToken cancellationToken); } diff --git a/ErsatzTV.Infrastructure/Data/DbInitializer.cs b/ErsatzTV.Infrastructure/Data/DbInitializer.cs index 1040e920a..4cb0c7ffa 100644 --- a/ErsatzTV.Infrastructure/Data/DbInitializer.cs +++ b/ErsatzTV.Infrastructure/Data/DbInitializer.cs @@ -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()) { diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs index 5b71cc9ac..2c70ed400 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs @@ -37,15 +37,15 @@ public class ChannelRepository : IChannelRepository .Map(Optional); } - public async Task> GetAll() + public async Task> 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> GetWatermarkByName(string name) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs index 651e4272a..60fd4b317 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs @@ -11,59 +11,62 @@ using Microsoft.Extensions.Logging; namespace ErsatzTV.Infrastructure.Data.Repositories; -public class EmbyTelevisionRepository : IEmbyTelevisionRepository +public class EmbyTelevisionRepository( + IDbContextFactory dbContextFactory, + ILogger logger) : IEmbyTelevisionRepository { - private readonly IDbContextFactory _dbContextFactory; - private readonly ILogger _logger; - - public EmbyTelevisionRepository( - IDbContextFactory dbContextFactory, - ILogger logger) + public async Task> GetExistingShows(EmbyLibrary library, CancellationToken cancellationToken) { - _dbContextFactory = dbContextFactory; - _logger = logger; - } - - public async Task> GetExistingShows(EmbyLibrary library) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); return await dbContext.Connection.QueryAsync( - @"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> GetExistingSeasons(EmbyLibrary library, EmbyShow show) + public async Task> 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( - @"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> GetExistingEpisodes(EmbyLibrary library, EmbySeason season) + public async Task> 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( - @"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 EmbyShow item, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybeExisting = await dbContext.EmbyShows .Include(m => m.LibraryPath) .ThenInclude(lp => lp.Library) @@ -97,14 +100,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository var result = new MediaItemScanResult(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>> GetOrAdd( @@ -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 maybeExisting = await dbContext.EmbySeasons .Include(m => m.LibraryPath) .Include(m => m.SeasonMetadata) @@ -126,14 +129,14 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository var result = new MediaItemScanResult(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>> GetOrAdd( @@ -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 maybeExisting = await dbContext.EmbyEpisodes .Include(m => m.LibraryPath) .ThenInclude(lp => lp.Library) @@ -178,7 +181,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository var result = new MediaItemScanResult(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 return await AddEpisode(dbContext, library, item, cancellationToken); } - public async Task SetEtag(EmbyShow show, string etag) + public async Task 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 SetEtag(EmbySeason season, string etag) + public async Task 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 SetEtag(EmbyEpisode episode, string etag) + public async Task 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> FlagNormal(EmbyLibrary library, EmbyEpisode episode) + public async Task> FlagNormal( + EmbyLibrary library, + EmbyEpisode episode, + CancellationToken cancellationToken) { if (episode.State is MediaItemState.Normal) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); episode.State = MediaItemState.Normal; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagNormal(EmbyLibrary library, EmbySeason season) + public async Task> FlagNormal( + EmbyLibrary library, + EmbySeason season, + CancellationToken cancellationToken) { if (season.State is MediaItemState.Normal) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); season.State = MediaItemState.Normal; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagNormal(EmbyLibrary library, EmbyShow show) + public async Task> FlagNormal(EmbyLibrary library, EmbyShow show, CancellationToken cancellationToken) { if (show.State is MediaItemState.Normal) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); show.State = MediaItemState.Normal; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagFileNotFoundShows(EmbyLibrary library, List showItemIds) + public async Task> FlagFileNotFoundShows( + EmbyLibrary library, + List showItemIds, + CancellationToken cancellationToken) { if (showItemIds.Count == 0) { return []; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); List ids = await dbContext.Connection.QueryAsync( - @"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> FlagFileNotFoundSeasons(EmbyLibrary library, List seasonItemIds) + public async Task> FlagFileNotFoundSeasons( + EmbyLibrary library, + List seasonItemIds, + CancellationToken cancellationToken) { if (seasonItemIds.Count == 0) { return []; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); List ids = await dbContext.Connection.QueryAsync( - @"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> FlagFileNotFoundEpisodes(EmbyLibrary library, List episodeItemIds) + public async Task> FlagFileNotFoundEpisodes( + EmbyLibrary library, + List episodeItemIds, + CancellationToken cancellationToken) { if (episodeItemIds.Count == 0) { return []; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); List ids = await dbContext.Connection.QueryAsync( - @"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> FlagUnavailable(EmbyLibrary library, EmbyEpisode episode) + public async Task> FlagUnavailable( + EmbyLibrary library, + EmbyEpisode episode, + CancellationToken cancellationToken) { if (episode.State is MediaItemState.Unavailable) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); episode.State = MediaItemState.Unavailable; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagRemoteOnly(EmbyLibrary library, EmbyEpisode episode) + public async Task> FlagRemoteOnly( + EmbyLibrary library, + EmbyEpisode episode, + CancellationToken cancellationToken) { if (episode.State is MediaItemState.RemoteOnly) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); episode.State = MediaItemState.RemoteOnly; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> GetShowTitleItemId(int libraryId, int showId) + public async Task> GetShowTitleItemId( + int libraryId, + int showId, + CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); Option 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 return Option.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 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 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 MediaFile incomingFile = incomingVersion.MediaFiles.Head(); file.Path = incomingFile.Path; - await dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); } private static async Task>> AddShow( TvContext dbContext, EmbyLibrary library, - EmbyShow show) + EmbyShow show, + CancellationToken cancellationToken) { try { @@ -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(show) { IsAdded = true }; } catch (Exception ex) @@ -854,7 +932,8 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository private static async Task>> AddSeason( TvContext dbContext, EmbyLibrary library, - EmbySeason season) + EmbySeason season, + CancellationToken cancellationToken) { try { @@ -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(season) { IsAdded = true }; } catch (Exception ex) @@ -892,7 +971,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository episode, library.Paths.Head().Id, dbContext, - _logger, + logger, cancellationToken)) { return new MediaFileAlreadyExists(); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs index e633c7353..b53ff14ee 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs @@ -24,46 +24,60 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository _logger = logger; } - public async Task> GetExistingShows(JellyfinLibrary library) + public async Task> 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( - @"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> GetExistingSeasons(JellyfinLibrary library, JellyfinShow show) + public async Task> 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( - @"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> GetExistingEpisodes(JellyfinLibrary library, JellyfinSeason season) + public async Task> 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( - @"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 var result = new MediaItemScanResult(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>> GetOrAdd( @@ -126,14 +140,14 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository var result = new MediaItemScanResult(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>> GetOrAdd( @@ -178,7 +192,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository var result = new MediaItemScanResult(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 return await AddEpisode(dbContext, library, item, cancellationToken); } - public async Task SetEtag(JellyfinShow show, string etag) + public async Task 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 SetEtag(JellyfinSeason season, string etag) + public async Task 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 SetEtag(JellyfinEpisode episode, string etag) + public async Task 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> FlagNormal(JellyfinLibrary library, JellyfinEpisode episode) + public async Task> FlagNormal( + JellyfinLibrary library, + JellyfinEpisode episode, + CancellationToken cancellationToken) { if (episode.State is MediaItemState.Normal) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); episode.State = MediaItemState.Normal; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagNormal(JellyfinLibrary library, JellyfinSeason season) + public async Task> FlagNormal( + JellyfinLibrary library, + JellyfinSeason season, + CancellationToken cancellationToken) { if (season.State is MediaItemState.Normal) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); season.State = MediaItemState.Normal; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagNormal(JellyfinLibrary library, JellyfinShow show) + public async Task> FlagNormal( + JellyfinLibrary library, + JellyfinShow show, + CancellationToken cancellationToken) { if (show.State is MediaItemState.Normal) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); show.State = MediaItemState.Normal; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagFileNotFoundShows(JellyfinLibrary library, List showItemIds) + public async Task> FlagFileNotFoundShows( + JellyfinLibrary library, + List showItemIds, + CancellationToken cancellationToken) { if (showItemIds.Count == 0) { return []; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); List ids = await dbContext.Connection.QueryAsync( - @"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> FlagFileNotFoundSeasons(JellyfinLibrary library, List seasonItemIds) + public async Task> FlagFileNotFoundSeasons( + JellyfinLibrary library, + List seasonItemIds, + CancellationToken cancellationToken) { if (seasonItemIds.Count == 0) { return []; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); List ids = await dbContext.Connection.QueryAsync( - @"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> FlagFileNotFoundEpisodes(JellyfinLibrary library, List episodeItemIds) + public async Task> FlagFileNotFoundEpisodes( + JellyfinLibrary library, + List episodeItemIds, + CancellationToken cancellationToken) { if (episodeItemIds.Count == 0) { return []; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); List ids = await dbContext.Connection.QueryAsync( - @"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> FlagUnavailable(JellyfinLibrary library, JellyfinEpisode episode) + public async Task> FlagUnavailable( + JellyfinLibrary library, + JellyfinEpisode episode, + CancellationToken cancellationToken) { if (episode.State is MediaItemState.Unavailable) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); episode.State = MediaItemState.Unavailable; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagRemoteOnly(JellyfinLibrary library, JellyfinEpisode episode) + public async Task> FlagRemoteOnly( + JellyfinLibrary library, + JellyfinEpisode episode, + CancellationToken cancellationToken) { if (episode.State is MediaItemState.RemoteOnly) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); episode.State = MediaItemState.RemoteOnly; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> GetShowTitleItemId(int libraryId, int showId) + public async Task> GetShowTitleItemId( + int libraryId, + int showId, + CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Option 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 return Option.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 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 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 MediaFile incomingFile = incomingVersion.MediaFiles.Head(); file.Path = incomingFile.Path; - await dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); } private static async Task>> AddShow( TvContext dbContext, JellyfinLibrary library, - JellyfinShow show) + JellyfinShow show, + CancellationToken cancellationToken) { try { @@ -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(show) { IsAdded = true }; } catch (Exception ex) @@ -854,7 +946,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository private static async Task>> AddSeason( TvContext dbContext, JellyfinLibrary library, - JellyfinSeason season) + JellyfinSeason season, + CancellationToken cancellationToken) { try { @@ -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(season) { IsAdded = true }; } catch (Exception ex) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs index f31fc41f5..9588e9c10 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs @@ -25,183 +25,227 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository _logger = logger; } - public async Task> FlagNormal(PlexLibrary library, PlexEpisode episode) + public async Task> FlagNormal( + PlexLibrary library, + PlexEpisode episode, + CancellationToken cancellationToken) { if (episode.State is MediaItemState.Normal) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); episode.State = MediaItemState.Normal; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagNormal(PlexLibrary library, PlexSeason season) + public async Task> FlagNormal( + PlexLibrary library, + PlexSeason season, + CancellationToken cancellationToken) { if (season.State is MediaItemState.Normal) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); season.State = MediaItemState.Normal; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagNormal(PlexLibrary library, PlexShow show) + public async Task> FlagNormal(PlexLibrary library, PlexShow show, CancellationToken cancellationToken) { if (show.State is MediaItemState.Normal) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); show.State = MediaItemState.Normal; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagUnavailable(PlexLibrary library, PlexEpisode episode) + public async Task> FlagUnavailable( + PlexLibrary library, + PlexEpisode episode, + CancellationToken cancellationToken) { if (episode.State is MediaItemState.Unavailable) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); episode.State = MediaItemState.Unavailable; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> FlagRemoteOnly(PlexLibrary library, PlexEpisode episode) + public async Task> FlagRemoteOnly( + PlexLibrary library, + PlexEpisode episode, + CancellationToken cancellationToken) { if (episode.State is MediaItemState.RemoteOnly) { return Option.None; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); episode.State = MediaItemState.RemoteOnly; Option maybeId = await dbContext.Connection.ExecuteScalarAsync( - @"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> GetExistingShows(PlexLibrary library) + public async Task> 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( - @"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> GetExistingSeasons(PlexLibrary library, PlexShow show) + public async Task> 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( - @"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> GetExistingEpisodes(PlexLibrary library, PlexSeason season) + public async Task> 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( - @"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 return new MediaItemScanResult(plexShow) { IsAdded = false }; } - return await AddShow(dbContext, library, item); + return await AddShow(dbContext, library, item, cancellationToken); } public async Task>> GetOrAdd( @@ -267,7 +311,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository return new MediaItemScanResult(plexSeason) { IsAdded = false }; } - return await AddSeason(dbContext, library, item); + return await AddSeason(dbContext, library, item, cancellationToken); } public async Task>> GetOrAdd( @@ -313,7 +357,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository var result = new MediaItemScanResult(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 return await AddEpisode(dbContext, library, item, cancellationToken); } - public async Task SetEtag(PlexShow show, string etag) + public async Task 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 SetEtag(PlexSeason season, string etag) + public async Task 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 SetEtag(PlexEpisode episode, string etag) + public async Task 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> FlagFileNotFoundShows(PlexLibrary library, List showItemIds) + public async Task> FlagFileNotFoundShows( + PlexLibrary library, + List showItemIds, + CancellationToken cancellationToken) { if (showItemIds.Count == 0) { return []; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); List ids = await dbContext.Connection.QueryAsync( - @"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> FlagFileNotFoundSeasons(PlexLibrary library, List seasonItemIds) + public async Task> FlagFileNotFoundSeasons( + PlexLibrary library, + List seasonItemIds, + CancellationToken cancellationToken) { if (seasonItemIds.Count == 0) { return []; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); List ids = await dbContext.Connection.QueryAsync( - @"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> FlagFileNotFoundEpisodes(PlexLibrary library, List episodeItemIds) + public async Task> FlagFileNotFoundEpisodes( + PlexLibrary library, + List episodeItemIds, + CancellationToken cancellationToken) { if (episodeItemIds.Count == 0) { return []; } - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); List ids = await dbContext.Connection.QueryAsync( - @"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 public async Task> RemoveAllTags( PlexLibrary library, PlexTag tag, - System.Collections.Generic.HashSet keep) + System.Collections.Generic.HashSet 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 .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 .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 AddTag(PlexLibrary library, PlexShow show, PlexTag tag) + public async Task 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( - @"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 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.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> GetShowTitleKey(int libraryId, int showId) + public async Task> GetShowTitleKey( + int libraryId, + int showId, + CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Option 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 private static async Task>> AddShow( TvContext dbContext, PlexLibrary library, - PlexShow item) + PlexShow item, + CancellationToken cancellationToken) { try { @@ -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(item) { IsAdded = true }; } catch (Exception ex) @@ -553,7 +643,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository private static async Task>> AddSeason( TvContext dbContext, PlexLibrary library, - PlexSeason item) + PlexSeason item, + CancellationToken cancellationToken) { try { @@ -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(item) { IsAdded = true }; } catch (Exception ex) @@ -632,7 +723,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository private async Task> UpdateEpisodePath( TvContext dbContext, PlexEpisode existing, - PlexEpisode incoming) + PlexEpisode incoming, + CancellationToken cancellationToken) { try { @@ -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.None; diff --git a/ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs index 773b22416..189fedb95 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs @@ -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( async () => await AddRemoteStream(dbContext, libraryPath.Id, libraryFolder.Id, path, cancellationToken)); } - public async Task> FindRemoteStreamPaths(LibraryPath libraryPath) + public async Task> 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( - @"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> DeleteByPath(LibraryPath libraryPath, string path) + public async Task> DeleteByPath(LibraryPath libraryPath, string path, CancellationToken cancellationToken) { - await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); List ids = await dbContext.Connection.QueryAsync( - @"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 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 AddTag(RemoteStreamMetadata metadata, Tag tag) + public async Task 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>> AddRemoteStream( diff --git a/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs b/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs index 746788ffe..b6b327e4a 100644 --- a/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs +++ b/ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyShowByIdHandler.cs @@ -74,7 +74,7 @@ public class SynchronizeEmbyShowByIdHandler : IRequestHandler (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 v.ToValidation($"Emby library {request.EmbyLibraryId} does not exist.")); private Task> EmbyShowMustExist( - SynchronizeEmbyShowById request) => - _embyTelevisionRepository.GetShowTitleItemId(request.EmbyLibraryId, request.ShowId) + SynchronizeEmbyShowById request, + CancellationToken cancellationToken) => + _embyTelevisionRepository.GetShowTitleItemId(request.EmbyLibraryId, request.ShowId, cancellationToken) .Map(v => v.ToValidation( $"Jellyfin show {request.ShowId} does not exist in library {request.EmbyLibraryId}.")); diff --git a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs index d2d575364..d5a61a36d 100644 --- a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs +++ b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs @@ -34,7 +34,7 @@ public class SynchronizeJellyfinShowById request, CancellationToken cancellationToken) { - Validation validation = await Validate(request); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( parameters => Synchronize(parameters, cancellationToken), error => Task.FromResult>(error.Join())); @@ -71,9 +71,11 @@ public class return result.Map(_ => $"Show '{parameters.ShowTitle}' in {parameters.Library.Name}"); } - private async Task> Validate(SynchronizeJellyfinShowById request) => + private async Task> 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 .Map(v => v.ToValidation($"Jellyfin library {request.JellyfinLibraryId} does not exist.")); private Task> JellyfinShowMustExist( - SynchronizeJellyfinShowById request) => - _jellyfinTelevisionRepository.GetShowTitleItemId(request.JellyfinLibraryId, request.ShowId) + SynchronizeJellyfinShowById request, + CancellationToken cancellationToken) => + _jellyfinTelevisionRepository.GetShowTitleItemId(request.JellyfinLibraryId, request.ShowId, cancellationToken) .Map(v => v.ToValidation( $"Jellyfin show {request.ShowId} does not exist in library {request.JellyfinLibraryId}.")); diff --git a/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexNetworksHandler.cs b/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexNetworksHandler.cs index 4a7bc58c2..3b35b605a 100644 --- a/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexNetworksHandler.cs +++ b/ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexNetworksHandler.cs @@ -107,7 +107,7 @@ public class SynchronizePlexNetworksHandler : IRequestHandler validation = await Validate(request); + Validation validation = await Validate(request, cancellationToken); return await validation.Match( parameters => Synchronize(parameters, cancellationToken), error => Task.FromResult>(error.Join())); @@ -70,8 +70,11 @@ public class SynchronizePlexShowByIdHandler : IRequestHandler $"Show '{parameters.ShowTitle}' in {parameters.Library.Name}"); } - private async Task> Validate(SynchronizePlexShowById request) => - (await ValidateConnection(request), await PlexLibraryMustExist(request), await PlexShowMustExist(request)) + private async Task> 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 v.ToValidation($"Plex library {request.PlexLibraryId} does not exist.")); private Task> PlexShowMustExist( - SynchronizePlexShowById request) => - _plexTelevisionRepository.GetShowTitleKey(request.PlexLibraryId, request.ShowId) + SynchronizePlexShowById request, + CancellationToken cancellationToken) => + _plexTelevisionRepository.GetShowTitleKey(request.PlexLibraryId, request.ShowId, cancellationToken) .Map(v => v.ToValidation( $"Plex show {request.ShowId} does not exist in library {request.PlexLibraryId}.")); diff --git a/ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs index 353cf8c4f..e0690a376 100644 --- a/ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs @@ -14,7 +14,7 @@ public interface ILocalMetadataProvider Task RefreshSidecarMetadata(OtherVideo otherVideo, string nfoFileName); Task RefreshTagMetadata(Song song); Task RefreshTagMetadata(Image image, double? durationSeconds); - Task RefreshTagMetadata(RemoteStream remoteStream); + Task RefreshTagMetadata(RemoteStream remoteStream, CancellationToken cancellationToken); Task RefreshFallbackMetadata(Movie movie); Task RefreshFallbackMetadata(Episode episode); Task RefreshFallbackMetadata(Artist artist, string artistFolder); @@ -22,6 +22,6 @@ public interface ILocalMetadataProvider Task RefreshFallbackMetadata(OtherVideo otherVideo); Task RefreshFallbackMetadata(Song song); Task RefreshFallbackMetadata(Image image); - Task RefreshFallbackMetadata(RemoteStream remoteStream); + Task RefreshFallbackMetadata(RemoteStream remoteStream, CancellationToken cancellationToken); Task RefreshFallbackMetadata(Show televisionShow, string showFolder); } diff --git a/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs index f1ef57ba5..9f4d4a2b6 100644 --- a/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs @@ -224,13 +224,13 @@ public class LocalMetadataProvider : ILocalMetadataProvider return await RefreshFallbackMetadata(image); } - public async Task RefreshTagMetadata(RemoteStream remoteStream) => + public async Task RefreshTagMetadata(RemoteStream remoteStream, CancellationToken cancellationToken) => // Option maybeMetadata = LoadRemoteStreamMetadata(remoteStream); // foreach (RemoteStreamMetadata metadata in maybeMetadata) // { // return await ApplyMetadataUpdate(remoteStream, metadata); // } - await RefreshFallbackMetadata(remoteStream); + await RefreshFallbackMetadata(remoteStream, cancellationToken); public Task RefreshFallbackMetadata(Movie movie) => ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie)); @@ -274,12 +274,12 @@ public class LocalMetadataProvider : ILocalMetadataProvider return false; } - public async Task RefreshFallbackMetadata(RemoteStream remoteStream) + public async Task RefreshFallbackMetadata(RemoteStream remoteStream, CancellationToken cancellationToken) { Option 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 return await _metadataRepository.Add(metadata); } - private async Task ApplyMetadataUpdate(RemoteStream remoteStream, RemoteStreamMetadata metadata) + private async Task ApplyMetadataUpdate( + RemoteStream remoteStream, + RemoteStreamMetadata metadata, + CancellationToken cancellationToken) { Option maybeMetadata = Optional(remoteStream.RemoteStreamMetadata).Flatten().HeadOrNone(); foreach (RemoteStreamMetadata existing in maybeMetadata) @@ -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)); diff --git a/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs index 3c844e4d3..f5464675e 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs @@ -91,7 +91,7 @@ public abstract class MediaServerTelevisionLibraryScanner(); - List existingShows = await televisionRepository.GetExistingShows(library); + List existingShows = await televisionRepository.GetExistingShows(library, cancellationToken); await foreach ((TShow incoming, int totalShowCount) in showEntries.WithCancellation(cancellationToken)) { @@ -147,9 +147,9 @@ public abstract class MediaServerTelevisionLibraryScanner flagResult = await televisionRepository.FlagNormal(library, result.Item); + Option flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken); if (flagResult.IsSome) { result.IsUpdated = true; @@ -173,7 +173,7 @@ public abstract class MediaServerTelevisionLibraryScanner s.MediaServerItemId).Except(incomingItemIds).ToList(); - List ids = await televisionRepository.FlagFileNotFoundShows(library, fileNotFoundItemIds); + List ids = await televisionRepository.FlagFileNotFoundShows(library, fileNotFoundItemIds, cancellationToken); await _mediator.Publish( new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty()), cancellationToken); @@ -296,7 +296,7 @@ public abstract class MediaServerTelevisionLibraryScanner(); - List existingSeasons = await televisionRepository.GetExistingSeasons(library, show); + List existingSeasons = await televisionRepository.GetExistingSeasons(library, show, cancellationToken); await foreach ((TSeason incoming, int _) in seasonEntries.WithCancellation(cancellationToken)) { @@ -346,9 +346,9 @@ public abstract class MediaServerTelevisionLibraryScanner flagResult = await televisionRepository.FlagNormal(library, result.Item); + Option flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken); if (flagResult.IsSome) { result.IsUpdated = true; @@ -372,7 +372,7 @@ public abstract class MediaServerTelevisionLibraryScanner s.MediaServerItemId).Except(incomingItemIds).ToList(); - List ids = await televisionRepository.FlagFileNotFoundSeasons(library, fileNotFoundItemIds); + List ids = await televisionRepository.FlagFileNotFoundSeasons(library, fileNotFoundItemIds, cancellationToken); await _mediator.Publish( new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty()), cancellationToken); @@ -393,7 +393,7 @@ public abstract class MediaServerTelevisionLibraryScanner(); - List existingEpisodes = await televisionRepository.GetExistingEpisodes(library, season); + List existingEpisodes = await televisionRepository.GetExistingEpisodes(library, season, cancellationToken); await foreach ((TEpisode incoming, int _) in episodeEntries.WithCancellation(cancellationToken)) { @@ -413,7 +413,8 @@ public abstract class MediaServerTelevisionLibraryScanner 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 flagResult = await televisionRepository.FlagNormal(library, result.Item); + Option flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken); if (flagResult.IsSome) { result.IsUpdated = true; @@ -497,7 +498,7 @@ public abstract class MediaServerTelevisionLibraryScanner flagResult = await televisionRepository.FlagRemoteOnly(library, result.Item); + Option flagResult = await televisionRepository.FlagRemoteOnly(library, result.Item, cancellationToken); if (flagResult.IsSome) { result.IsUpdated = true; @@ -505,7 +506,7 @@ public abstract class MediaServerTelevisionLibraryScanner flagResult = await televisionRepository.FlagUnavailable(library, result.Item); + Option flagResult = await televisionRepository.FlagUnavailable(library, result.Item, cancellationToken); if (flagResult.IsSome) { result.IsUpdated = true; @@ -528,7 +529,7 @@ public abstract class MediaServerTelevisionLibraryScanner m.MediaServerItemId).Except(incomingItemIds).ToList(); - List ids = await televisionRepository.FlagFileNotFoundEpisodes(library, fileNotFoundItemIds); + List ids = await televisionRepository.FlagFileNotFoundEpisodes(library, fileNotFoundItemIds, cancellationToken); await _mediator.Publish( new ScannerProgressUpdate(library.Id, null, null, ids.ToArray(), Array.Empty()), cancellationToken); @@ -544,7 +545,8 @@ public abstract class MediaServerTelevisionLibraryScanner 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()), + new ScannerProgressUpdate(library.Id, null, null, [id], []), CancellationToken.None); } } @@ -587,10 +589,10 @@ public abstract class MediaServerTelevisionLibraryScanner()), + new ScannerProgressUpdate(library.Id, null, null, [id], []), CancellationToken.None); } } diff --git a/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs index 47b6e2a2f..04059f406 100644 --- a/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs @@ -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 } } - 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 else if (Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("Removing dot underscore file at {Path}", path); - List remoteStreamIds = await _remoteStreamRepository.DeleteByPath(libraryPath, path); + List remoteStreamIds = await _remoteStreamRepository.DeleteByPath(libraryPath, path, cancellationToken); await _mediator.Publish( new ScannerProgressUpdate( libraryPath.LibraryId, @@ -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 } private async Task>> UpdateMetadata( - MediaItemScanResult result) + MediaItemScanResult result, + CancellationToken cancellationToken) { try { @@ -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; } diff --git a/ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs index 8c03843dc..0bee6044a 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexNetworkScanner.cs @@ -58,7 +58,7 @@ public class PlexNetworkScanner( var keepIds = new System.Collections.Generic.HashSet(); 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( cancellationToken.ThrowIfCancellationRequested(); } - List removedIds = await plexTelevisionRepository.RemoveAllTags(library, tag, keepIds); + List removedIds = await plexTelevisionRepository.RemoveAllTags(library, tag, keepIds, cancellationToken); var changedIds = removedIds.Concat(addedIds).Distinct().ToList(); if (changedIds.Count > 0) diff --git a/ErsatzTV/Pages/Artist.razor b/ErsatzTV/Pages/Artist.razor index 1b5013ea9..f00b31384 100644 --- a/ErsatzTV/Pages/Artist.razor +++ b/ErsatzTV/Pages/Artist.razor @@ -243,7 +243,7 @@ @code { - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts; [Parameter] public int ArtistId { get; set; } @@ -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() diff --git a/ErsatzTV/Pages/BlockEditor.razor b/ErsatzTV/Pages/BlockEditor.razor index dc83e90bf..a7550355c 100644 --- a/ErsatzTV/Pages/BlockEditor.razor +++ b/ErsatzTV/Pages/BlockEditor.razor @@ -314,7 +314,7 @@ else if (_previewItems != null) @code { - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts; [Parameter] public int Id { get; set; } @@ -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 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 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> maybeResults = await Mediator.Send(new GetBlockItems(Id), _cts.Token); - foreach (IEnumerable 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> maybeResults = await Mediator.Send(new GetBlockItems(Id), token); + foreach (IEnumerable items in maybeResults) + { + _block.Items.AddRange(items.Map(ProjectToEditViewModel)); + if (_block.Items.Count == 1) + { + _selectedItem = _block.Items.Head(); + } + } + } + catch (OperationCanceledException) + { + // do nothing } } diff --git a/ErsatzTV/Pages/BlockPlayoutEditor.razor b/ErsatzTV/Pages/BlockPlayoutEditor.razor index 8f2a25224..49ad80f2e 100644 --- a/ErsatzTV/Pages/BlockPlayoutEditor.razor +++ b/ErsatzTV/Pages/BlockPlayoutEditor.razor @@ -84,7 +84,7 @@ @code { - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts; [Parameter] public int Id { get; set; } @@ -99,34 +99,46 @@ public void Dispose() { - _cts.Cancel(); - _cts.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); } protected override async Task OnParametersSetAsync() { - Option 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 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 maybeDefaultDeco = await Mediator.Send(new GetDecoByPlayoutId(Id), _cts.Token); - foreach (DecoViewModel defaultDeco in maybeDefaultDeco) + Option 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 } } diff --git a/ErsatzTV/Pages/Blocks.razor b/ErsatzTV/Pages/Blocks.razor index 4cde9d427..1d0cd6ab8 100644 --- a/ErsatzTV/Pages/Blocks.razor +++ b/ErsatzTV/Pages/Blocks.razor @@ -84,7 +84,7 @@ @code { - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts; private readonly List> _treeItems = []; private List _blockGroups = []; private BlockGroupViewModel _selectedBlockGroup; @@ -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 { Value = new BlockTreeItemViewModel(group) }); + } + } + catch (OperationCanceledException) { - _treeItems.Add(new TreeItemData { Value = new BlockTreeItemViewModel(group) }); + // do nothing } } diff --git a/ErsatzTV/Pages/ChannelEditor.razor b/ErsatzTV/Pages/ChannelEditor.razor index d48a87742..088f1d841 100644 --- a/ErsatzTV/Pages/ChannelEditor.razor +++ b/ErsatzTV/Pages/ChannelEditor.razor @@ -20,7 +20,7 @@ - @(IsEdit ? "Save Channel" : "Add Channel") + @(IsEdit ? "Save Channel" : "Add Channel")
@@ -221,7 +221,7 @@ else
Logo
-
@code { - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts; [CascadingParameter] IMudDialogInstance MudDialog { get; set; } @@ -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}"; diff --git a/ErsatzTV/Shared/CopyFFmpegProfileDialog.razor b/ErsatzTV/Shared/CopyFFmpegProfileDialog.razor index b88d1edea..64cb5c370 100644 --- a/ErsatzTV/Shared/CopyFFmpegProfileDialog.razor +++ b/ErsatzTV/Shared/CopyFFmpegProfileDialog.razor @@ -19,8 +19,8 @@ - Cancel - + Cancel + Copy Profile @@ -43,8 +43,8 @@ public void Dispose() { - _cts.Cancel(); - _cts.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); } private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newFFmpegProfileName); diff --git a/ErsatzTV/Shared/CopyScheduleDialog.razor b/ErsatzTV/Shared/CopyScheduleDialog.razor index 986c0fdaa..b9ac659d6 100644 --- a/ErsatzTV/Shared/CopyScheduleDialog.razor +++ b/ErsatzTV/Shared/CopyScheduleDialog.razor @@ -19,8 +19,8 @@ - Cancel - + Cancel + Copy Schedule @@ -43,8 +43,8 @@ public void Dispose() { - _cts.Cancel(); - _cts.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); } private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newName); diff --git a/ErsatzTV/Shared/CopyWatermarkDialog.razor b/ErsatzTV/Shared/CopyWatermarkDialog.razor index 89e630a6f..0212c1509 100644 --- a/ErsatzTV/Shared/CopyWatermarkDialog.razor +++ b/ErsatzTV/Shared/CopyWatermarkDialog.razor @@ -19,8 +19,8 @@ - Cancel - + Cancel + Copy Watermark @@ -43,8 +43,8 @@ public void Dispose() { - _cts.Cancel(); - _cts.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); } private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newWatermarkName); diff --git a/ErsatzTV/Shared/EditExternalJsonFileDialog.razor b/ErsatzTV/Shared/EditExternalJsonFileDialog.razor index c9dfefd74..b78a2fec1 100644 --- a/ErsatzTV/Shared/EditExternalJsonFileDialog.razor +++ b/ErsatzTV/Shared/EditExternalJsonFileDialog.razor @@ -1,6 +1,4 @@ -@implements IDisposable - - + @@ -10,16 +8,14 @@ - Cancel - + Cancel + Save Changes @code { - private readonly CancellationTokenSource _cts = new(); - [Parameter] public string ExternalJsonFile { get; set; } @@ -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)); diff --git a/ErsatzTV/Shared/EditImageFolderDurationDialog.razor b/ErsatzTV/Shared/EditImageFolderDurationDialog.razor index 26f906d10..2fe54a056 100644 --- a/ErsatzTV/Shared/EditImageFolderDurationDialog.razor +++ b/ErsatzTV/Shared/EditImageFolderDurationDialog.razor @@ -1,6 +1,4 @@ -@implements IDisposable - - + @@ -15,16 +13,14 @@ Immediate="true"/> - Cancel - + Cancel + Save Changes @code { - private readonly CancellationTokenSource _cts = new(); - [Parameter] public double? ImageFolderDuration { get; set; } @@ -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)); diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index 43ef4ab28..6eb6d4faa 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -215,7 +215,7 @@ @code { private static readonly string InfoVersion = Assembly.GetEntryAssembly().GetCustomAttribute()?.InformationalVersion ?? "unknown"; - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts; private string _query; @@ -266,8 +266,8 @@ Courier.UnSubscribe(HandleHealthCheckSummary); - _cts.Cancel(); - _cts.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); } private static MudTheme ErsatzTvTheme => new() @@ -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() { diff --git a/ErsatzTV/Shared/MoveLocalLibraryPathDialog.razor b/ErsatzTV/Shared/MoveLocalLibraryPathDialog.razor index 72514ab1e..e4a0b8e59 100644 --- a/ErsatzTV/Shared/MoveLocalLibraryPathDialog.razor +++ b/ErsatzTV/Shared/MoveLocalLibraryPathDialog.razor @@ -37,7 +37,7 @@ @code { - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts; [CascadingParameter] IMudDialogInstance MudDialog { get; set; } @@ -61,8 +61,8 @@ public void Dispose() { - _cts.Cancel(); - _cts.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); } private bool CanSubmit() => @@ -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 } } diff --git a/ErsatzTV/Shared/RemoteMediaSourceEditor.razor b/ErsatzTV/Shared/RemoteMediaSourceEditor.razor index 19a143cfe..fd276a665 100644 --- a/ErsatzTV/Shared/RemoteMediaSourceEditor.razor +++ b/ErsatzTV/Shared/RemoteMediaSourceEditor.razor @@ -1,4 +1,4 @@ -@inject IMediator Mediator +@implements IDisposable @inject NavigationManager NavigationManager @inject ISnackbar Snackbar @inject ILogger Logger @@ -24,21 +24,43 @@ @code { + private CancellationTokenSource _cts; [Parameter] public string Name { get; set; } [Parameter] - public Func> LoadSecrets { get; set; } + public Func> LoadSecrets { get; set; } [Parameter] - public Func>> SaveSecrets { get; set; } + public Func>> 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 @@ _messageStore.Clear(); if (_editContext.Validate()) { - Either result = await SaveSecrets(_model); + Either result = await SaveSecrets(_model, _cts.Token); result.Match( _ => NavigationManager.NavigateTo($"media/sources/{Name.ToLowerInvariant()}"), error => diff --git a/ErsatzTV/Shared/RemoteMediaSourceLibrariesEditor.razor b/ErsatzTV/Shared/RemoteMediaSourceLibrariesEditor.razor index 687693666..2391a01cf 100644 --- a/ErsatzTV/Shared/RemoteMediaSourceLibrariesEditor.razor +++ b/ErsatzTV/Shared/RemoteMediaSourceLibrariesEditor.razor @@ -77,8 +77,8 @@ public void Dispose() { - _cts.Cancel(); - _cts.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); } protected override Task OnParametersSetAsync() => LoadData(); diff --git a/ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor b/ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor index 47224eb2e..c8bad4ee9 100644 --- a/ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor +++ b/ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor @@ -79,7 +79,7 @@
@code { - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts; [Parameter] public int Id { get; set; } @@ -88,10 +88,10 @@ public string Name { get; set; } [Parameter] - public Func>> GetMediaSourceById { get; set; } + public Func>> GetMediaSourceById { get; set; } [Parameter] - public Func>> GetPathReplacementsBySourceId { get; set; } + public Func>> GetPathReplacementsBySourceId { get; set; } [Parameter] public Func, IRequest>> GetUpdatePathReplacementsRequest { get; set; } @@ -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 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 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() diff --git a/ErsatzTV/Shared/RemoteMediaSources.razor b/ErsatzTV/Shared/RemoteMediaSources.razor index bfea4689a..205c31353 100644 --- a/ErsatzTV/Shared/RemoteMediaSources.razor +++ b/ErsatzTV/Shared/RemoteMediaSources.razor @@ -63,7 +63,7 @@ + OnClick="@(_ => RefreshLibraries(context.Id, _cts?.Token ?? CancellationToken.None))"> @@ -85,7 +85,7 @@ @code { - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts; [Parameter] public string Name { get; set; } @@ -97,28 +97,43 @@ public IRequest> DisconnectCommand { get; set; } [Parameter] - public Func RefreshLibrariesCommand { get; set; } + public Func RefreshLibrariesCommand { get; set; } [Parameter] public IRemoteMediaSourceSecretStore SecretStore { get; set; } - private List _mediaSources = new(); + private List _mediaSources = []; private bool _isAuthorized; public void Dispose() { - _cts.Cancel(); - _cts.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); } - protected override async Task OnParametersSetAsync() => await LoadMediaSources(); + protected override async Task OnParametersSetAsync() + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + var token = _cts.Token; + + try + { + await LoadMediaSources(token); + } + catch (OperationCanceledException) + { + // do nothing + } + } - private async Task LoadMediaSources() + private async Task LoadMediaSources(CancellationToken cancellationToken) { _isAuthorized = await SecretStore.ReadSecrets() .Map(secrets => !string.IsNullOrWhiteSpace(secrets.Address) && !string.IsNullOrWhiteSpace(secrets.ApiKey)); - _mediaSources = await Mediator.Send(GetAllMediaSourcesCommand, _cts.Token); + _mediaSources = await Mediator.Send(GetAllMediaSourcesCommand, cancellationToken); } private async Task Disconnect() @@ -132,11 +147,12 @@ if (Locker.LockRemoteMediaSource()) { await Mediator.Send(DisconnectCommand, _cts.Token); - await LoadMediaSources(); + await LoadMediaSources(_cts.Token); } } } - private async Task RefreshLibraries(int mediaSourceId) => await RefreshLibrariesCommand(mediaSourceId); + private async Task RefreshLibraries(int mediaSourceId, CancellationToken cancellationToken) => + await RefreshLibrariesCommand(mediaSourceId, cancellationToken); } diff --git a/ErsatzTV/Shared/SaveAsSmartCollectionDialog.razor b/ErsatzTV/Shared/SaveAsSmartCollectionDialog.razor index 87760d338..339e3f3e6 100644 --- a/ErsatzTV/Shared/SaveAsSmartCollectionDialog.razor +++ b/ErsatzTV/Shared/SaveAsSmartCollectionDialog.razor @@ -38,7 +38,7 @@ @code { - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts; [CascadingParameter] IMudDialogInstance MudDialog { get; set; } @@ -59,8 +59,8 @@ public void Dispose() { - _cts.Cancel(); - _cts.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); } private bool CanSubmit() => @@ -68,16 +68,28 @@ protected override async Task OnParametersSetAsync() { - _collections = await Mediator.Send(new GetAllSmartCollections(), _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("SaveAsSmartCollectionDialog.SelectedCollectionId", out int id)) + try { - _selectedCollection = _collections.SingleOrDefault(c => c.Id == id) ?? _newCollection; + _collections = await Mediator.Send(new GetAllSmartCollections(), token) + .Map(list => new[] { _newCollection }.Append(list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase)).ToList()); + + if (MemoryCache.TryGetValue("SaveAsSmartCollectionDialog.SelectedCollectionId", out int id)) + { + _selectedCollection = _collections.SingleOrDefault(c => c.Id == id) ?? _newCollection; + } + else + { + _selectedCollection = _newCollection; + } } - else + catch (OperationCanceledException) { - _selectedCollection = _newCollection; + // do nothing } } diff --git a/ErsatzTV/Shared/SchedulePlayoutReset.razor b/ErsatzTV/Shared/SchedulePlayoutReset.razor index 4067437be..5bcb4ed57 100644 --- a/ErsatzTV/Shared/SchedulePlayoutReset.razor +++ b/ErsatzTV/Shared/SchedulePlayoutReset.razor @@ -24,8 +24,8 @@ - Cancel - + Cancel + Save Changes @@ -59,8 +59,8 @@ public void Dispose() { - _cts.Cancel(); - _cts.Dispose(); + _cts?.Cancel(); + _cts?.Dispose(); } protected override void OnParametersSet() => _resetTime = DailyResetTime;