diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs index fbbe4502b..9b8c25fb7 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs @@ -46,8 +46,8 @@ public void SetUp() _xemEpisodes = new List(); Mocker.GetMock() - .Setup(v => v.GetSeries(_xemSeries.Id)) - .Returns(_xemSeries); + .Setup(v => v.GetSeriesAsync(_xemSeries.Id)) + .ReturnsAsync(_xemSeries); Mocker.GetMock() .Setup(v => v.GetEpisodesBySeason(_xemSeries.Id, It.IsAny())) @@ -205,8 +205,8 @@ public async Task Tags_IndexerAndSeriesTagsMatch_IndexerIncluded() .Build(); Mocker.GetMock() - .Setup(v => v.GetSeries(_xemSeries.Id)) - .Returns(_xemSeries); + .Setup(v => v.GetSeriesAsync(_xemSeries.Id)) + .ReturnsAsync(_xemSeries); WithEpisodes(); diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index 3af963f78..6f0c4151f 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -58,7 +58,7 @@ public async Task> EpisodeSearch(int episodeId, bool user public async Task> EpisodeSearch(Episode episode, bool userInvokedSearch, bool interactiveSearch) { - var series = _seriesService.GetSeries(episode.SeriesId); + var series = await _seriesService.GetSeriesAsync(episode.SeriesId); if (series.SeriesType == SeriesTypes.Daily) { @@ -113,7 +113,7 @@ public async Task> SeasonSearch(int seriesId, int seasonN public async Task> SeasonSearch(int seriesId, int seasonNumber, List episodes, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch) { - var series = _seriesService.GetSeries(seriesId); + var series = await _seriesService.GetSeriesAsync(seriesId); if (series.SeriesType == SeriesTypes.Anime) { diff --git a/src/NzbDrone.Core/Tv/AddSeriesService.cs b/src/NzbDrone.Core/Tv/AddSeriesService.cs index 4afa17f4d..ad151913d 100644 --- a/src/NzbDrone.Core/Tv/AddSeriesService.cs +++ b/src/NzbDrone.Core/Tv/AddSeriesService.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using FluentValidation; using FluentValidation.Results; using NLog; @@ -17,6 +19,7 @@ namespace NzbDrone.Core.Tv public interface IAddSeriesService { Series AddSeries(Series newSeries); + Task AddSeriesAsync(Series newSeries, CancellationToken cancellationToken = default); List AddSeries(List newSeries, bool ignoreErrors = false); } @@ -54,6 +57,19 @@ public Series AddSeries(Series newSeries) return newSeries; } + public async Task AddSeriesAsync(Series newSeries, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(newSeries); + + newSeries = AddSkyhookData(newSeries); + newSeries = SetPropertiesAndValidate(newSeries); + + _logger.Info("Adding Series {0} Path: [{1}]", newSeries, newSeries.Path); + await _seriesService.AddSeriesAsync(newSeries, cancellationToken); + + return newSeries; + } + public List AddSeries(List newSeries, bool ignoreErrors = false) { var added = DateTime.UtcNow; diff --git a/src/NzbDrone.Core/Tv/SeriesRepository.cs b/src/NzbDrone.Core/Tv/SeriesRepository.cs index a4f6e6365..6959c90f9 100644 --- a/src/NzbDrone.Core/Tv/SeriesRepository.cs +++ b/src/NzbDrone.Core/Tv/SeriesRepository.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Dapper; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -9,17 +11,29 @@ namespace NzbDrone.Core.Tv public interface ISeriesRepository : IBasicRepository { bool SeriesPathExists(string path); + Task SeriesPathExistsAsync(string path, CancellationToken cancellationToken = default); Series FindByTitle(string cleanTitle); + Task FindByTitleAsync(string cleanTitle, CancellationToken cancellationToken = default); Series FindByTitle(string cleanTitle, int year); + Task FindByTitleAsync(string cleanTitle, int year, CancellationToken cancellationToken = default); List FindByTitleInexact(string cleanTitle); + Task> FindByTitleInexactAsync(string cleanTitle, CancellationToken cancellationToken = default); Series FindByTvdbId(int tvdbId); + Task FindByTvdbIdAsync(int tvdbId, CancellationToken cancellationToken = default); Series FindByTvRageId(int tvRageId); + Task FindByTvRageIdAsync(int tvRageId, CancellationToken cancellationToken = default); Series FindByImdbId(string imdbId); + Task FindByImdbIdAsync(string imdbId, CancellationToken cancellationToken = default); Series FindByPath(string path); + Task FindByPathAsync(string path, CancellationToken cancellationToken = default); List AllSeriesTvdbIds(); + Task> AllSeriesTvdbIdsAsync(CancellationToken cancellationToken = default); Dictionary AllSeriesPaths(); + Task> AllSeriesPathsAsync(CancellationToken cancellationToken = default); Dictionary> AllSeriesTags(); + Task>> AllSeriesTagsAsync(CancellationToken cancellationToken = default); Dictionary AllSeriesQualityProfiles(); + Task> AllSeriesQualityProfilesAsync(CancellationToken cancellationToken = default); } public class SeriesRepository : BasicRepository, ISeriesRepository @@ -34,6 +48,11 @@ public bool SeriesPathExists(string path) return Query(c => c.Path == path).Any(); } + public async Task SeriesPathExistsAsync(string path, CancellationToken cancellationToken = default) + { + return await QueryAsync(c => c.Path == path, cancellationToken).AnyAsync(cancellationToken); + } + public Series FindByTitle(string cleanTitle) { cleanTitle = cleanTitle.ToLowerInvariant(); @@ -44,6 +63,15 @@ public Series FindByTitle(string cleanTitle) return ReturnSingleSeriesOrThrow(series); } + public async Task FindByTitleAsync(string cleanTitle, CancellationToken cancellationToken = default) + { + cleanTitle = cleanTitle.ToLowerInvariant(); + + var series = await QueryAsync(s => s.CleanTitle == cleanTitle, cancellationToken).ToListAsync(cancellationToken); + + return ReturnSingleSeriesOrThrow(series); + } + public Series FindByTitle(string cleanTitle, int year) { cleanTitle = cleanTitle.ToLowerInvariant(); @@ -53,6 +81,15 @@ public Series FindByTitle(string cleanTitle, int year) return ReturnSingleSeriesOrThrow(series); } + public async Task FindByTitleAsync(string cleanTitle, int year, CancellationToken cancellationToken = default) + { + cleanTitle = cleanTitle.ToLowerInvariant(); + + var series = await QueryAsync(s => s.CleanTitle == cleanTitle && s.Year == year, cancellationToken).ToListAsync(cancellationToken); + + return ReturnSingleSeriesOrThrow(series); + } + public List FindByTitleInexact(string cleanTitle) { var builder = Builder().Where($"instr(@cleanTitle, \"Series\".\"CleanTitle\")", new { cleanTitle = cleanTitle }); @@ -65,27 +102,59 @@ public List FindByTitleInexact(string cleanTitle) return Query(builder).ToList(); } + public async Task> FindByTitleInexactAsync(string cleanTitle, CancellationToken cancellationToken = default) + { + var builder = Builder().Where("instr(@cleanTitle, \"Series\".\"CleanTitle\")", new { cleanTitle = cleanTitle }); + + if (_database.DatabaseType == DatabaseType.PostgreSQL) + { + builder = Builder().Where("(strpos(@cleanTitle, \"Series\".\"CleanTitle\") > 0)", new { cleanTitle = cleanTitle }); + } + + return await QueryAsync(builder, cancellationToken).ToListAsync(cancellationToken); + } + public Series FindByTvdbId(int tvdbId) { return Query(s => s.TvdbId == tvdbId).SingleOrDefault(); } + public async Task FindByTvdbIdAsync(int tvdbId, CancellationToken cancellationToken = default) + { + return await QueryAsync(s => s.TvdbId == tvdbId, cancellationToken).SingleOrDefaultAsync(cancellationToken); + } + public Series FindByTvRageId(int tvRageId) { return Query(s => s.TvRageId == tvRageId).SingleOrDefault(); } + public async Task FindByTvRageIdAsync(int tvRageId, CancellationToken cancellationToken = default) + { + return await QueryAsync(s => s.TvRageId == tvRageId, cancellationToken).SingleOrDefaultAsync(cancellationToken); + } + public Series FindByImdbId(string imdbId) { return Query(s => s.ImdbId == imdbId).SingleOrDefault(); } + public async Task FindByImdbIdAsync(string imdbId, CancellationToken cancellationToken = default) + { + return await QueryAsync(s => s.ImdbId == imdbId, cancellationToken).SingleOrDefaultAsync(cancellationToken); + } + public Series FindByPath(string path) { return Query(s => s.Path == path) .FirstOrDefault(); } + public async Task FindByPathAsync(string path, CancellationToken cancellationToken = default) + { + return await QueryAsync(s => s.Path == path, cancellationToken).SingleOrDefaultAsync(cancellationToken); + } + public List AllSeriesTvdbIds() { using (var conn = _database.OpenConnection()) @@ -94,6 +163,13 @@ public List AllSeriesTvdbIds() } } + public async Task> AllSeriesTvdbIdsAsync(CancellationToken cancellationToken = default) + { + await using var conn = await _database.OpenConnectionAsync(cancellationToken); + + return await conn.QueryUnbufferedAsync("SELECT \"TvdbId\" FROM \"Series\"").ToListAsync(cancellationToken); + } + public Dictionary AllSeriesPaths() { using (var conn = _database.OpenConnection()) @@ -103,6 +179,14 @@ public Dictionary AllSeriesPaths() } } + public async Task> AllSeriesPathsAsync(CancellationToken cancellationToken = default) + { + await using var conn = await _database.OpenConnectionAsync(cancellationToken); + + return await conn.QueryUnbufferedAsync>("SELECT \"Id\" AS Key, \"Path\" AS Value FROM \"Series\"") + .ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken: cancellationToken); + } + public Dictionary> AllSeriesTags() { using (var conn = _database.OpenConnection()) @@ -112,6 +196,14 @@ public Dictionary> AllSeriesTags() } } + public async Task>> AllSeriesTagsAsync(CancellationToken cancellationToken = default) + { + await using var conn = await _database.OpenConnectionAsync(cancellationToken); + + return await conn.QueryUnbufferedAsync>>("SELECT \"Id\" AS Key, \"Tags\" AS Value FROM \"Series\" WHERE \"Tags\" IS NOT NULL") + .ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken: cancellationToken); + } + public Dictionary AllSeriesQualityProfiles() { using (var conn = _database.OpenConnection()) @@ -121,6 +213,14 @@ public Dictionary AllSeriesQualityProfiles() } } + public async Task> AllSeriesQualityProfilesAsync(CancellationToken cancellationToken = default) + { + await using var conn = await _database.OpenConnectionAsync(cancellationToken); + + return await conn.QueryUnbufferedAsync>("SELECT \"Id\" AS Key, \"QualityProfileId\" AS Value FROM \"Series\"") + .ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken: cancellationToken); + } + private Series ReturnSingleSeriesOrThrow(List series) { if (series.Count == 0) diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index 2430cb759..7d4f65ce8 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.AutoTagging; @@ -12,27 +14,46 @@ namespace NzbDrone.Core.Tv public interface ISeriesService { Series GetSeries(int seriesId); + Task GetSeriesAsync(int seriesId, CancellationToken cancellationToken = default); List GetSeries(IEnumerable seriesIds); + IAsyncEnumerable GetSeriesAsync(IEnumerable seriesIds, CancellationToken cancellationToken = default); Series AddSeries(Series newSeries); + Task AddSeriesAsync(Series newSeries, CancellationToken cancellationToken = default); List AddSeries(List newSeries); + Task> AddSeriesAsync(List newSeries, CancellationToken cancellationToken = default); Series FindByTvdbId(int tvdbId); + Task FindByTvdbIdAsync(int tvRageId, CancellationToken cancellationToken = default); Series FindByTvRageId(int tvRageId); + Task FindByTvRageIdAsync(int tvRageId, CancellationToken cancellationToken = default); Series FindByImdbId(string imdbId); + Task FindByImdbIdAsync(string imdbId, CancellationToken cancellationToken = default); Series FindByTitle(string title); + Task FindByTitleAsync(string title, CancellationToken cancellationToken = default); Series FindByTitle(string title, int year); + Task FindByTitleAsync(string title, int year, CancellationToken cancellationToken = default); Series FindByTitleInexact(string title); Series FindByPath(string path); + Task FindByPathAsync(string path, CancellationToken cancellationToken = default); void DeleteSeries(List seriesIds, bool deleteFiles, bool addImportListExclusion); + Task DeleteSeriesAsync(List seriesIds, bool deleteFiles, bool addImportListExclusion, CancellationToken cancellationToken = default); List GetAllSeries(); + IAsyncEnumerable GetAllSeriesAsync(CancellationToken cancellationToken = default); List AllSeriesTvdbIds(); + Task> AllSeriesTvdbIdsAsync(CancellationToken cancellation = default); Dictionary GetAllSeriesPaths(); + Task> GetAllSeriesPathsAsync(CancellationToken cancellationToken = default); Dictionary> GetAllSeriesTags(); + Task>> GetAllSeriesTagsAsync(CancellationToken cancellationToken = default); List AllForTag(int tagId); + IAsyncEnumerable AllForTagAsync(int tagId, CancellationToken cancellationToken = default); Dictionary GetAllSeriesQualityProfiles(); + Task> GetAllSeriesQualityProfilesAsync(CancellationToken cancellationToken = default); Series UpdateSeries(Series series, bool updateEpisodesToMatchSeason = true, bool publishUpdatedEvent = true); List UpdateSeries(List series, bool useExistingRelativeFolder); bool SeriesPathExists(string folder); + Task SeriesPathExistsAsync(string folder, CancellationToken cancellationToken = default); void RemoveAddOptions(Series series); + Task RemoveAddOptionsAsync(Series series, CancellationToken cancellationToken = default); bool UpdateTags(Series series); } @@ -65,11 +86,21 @@ public Series GetSeries(int seriesId) return _seriesRepository.Get(seriesId); } + public async Task GetSeriesAsync(int seriesId, CancellationToken cancellationToken = default) + { + return await _seriesRepository.GetAsync(seriesId, cancellationToken); + } + public List GetSeries(IEnumerable seriesIds) { return _seriesRepository.Get(seriesIds).ToList(); } + public IAsyncEnumerable GetSeriesAsync(IEnumerable seriesIds, CancellationToken cancellationToken = default) + { + return _seriesRepository.GetAsync(seriesIds, cancellationToken); + } + public Series AddSeries(Series newSeries) { _seriesRepository.Insert(newSeries); @@ -78,6 +109,14 @@ public Series AddSeries(Series newSeries) return newSeries; } + public async Task AddSeriesAsync(Series newSeries, CancellationToken cancellationToken = default) + { + await _seriesRepository.InsertAsync(newSeries, cancellationToken); + _eventAggregator.PublishEvent(new SeriesAddedEvent(await GetSeriesAsync(newSeries.Id, cancellationToken))); + + return newSeries; + } + public List AddSeries(List newSeries) { _seriesRepository.InsertMany(newSeries); @@ -86,26 +125,54 @@ public List AddSeries(List newSeries) return newSeries; } + public async Task> AddSeriesAsync(List newSeries, CancellationToken cancellationToken = default) + { + await _seriesRepository.InsertManyAsync(newSeries, cancellationToken); + _eventAggregator.PublishEvent(new SeriesImportedEvent(newSeries.Select(s => s.Id).ToList())); + + return newSeries; + } + public Series FindByTvdbId(int tvRageId) { return _seriesRepository.FindByTvdbId(tvRageId); } + public async Task FindByTvdbIdAsync(int tvRageId, CancellationToken cancellationToken = default) + { + return await _seriesRepository.FindByTvdbIdAsync(tvRageId, cancellationToken); + } + public Series FindByTvRageId(int tvRageId) { return _seriesRepository.FindByTvRageId(tvRageId); } + public async Task FindByTvRageIdAsync(int tvRageId, CancellationToken cancellationToken = default) + { + return await _seriesRepository.FindByTvRageIdAsync(tvRageId, cancellationToken); + } + public Series FindByImdbId(string imdbId) { return _seriesRepository.FindByImdbId(imdbId); } + public async Task FindByImdbIdAsync(string imdbId, CancellationToken cancellationToken = default) + { + return await _seriesRepository.FindByImdbIdAsync(imdbId, cancellationToken); + } + public Series FindByTitle(string title) { return _seriesRepository.FindByTitle(title.CleanSeriesTitle()); } + public async Task FindByTitleAsync(string title, CancellationToken cancellationToken = default) + { + return await _seriesRepository.FindByTitleAsync(title.CleanSeriesTitle(), cancellationToken); + } + public Series FindByTitleInexact(string title) { // find any series clean title within the provided release title @@ -155,11 +222,21 @@ public Series FindByPath(string path) return _seriesRepository.FindByPath(path); } + public async Task FindByPathAsync(string path, CancellationToken cancellationToken = default) + { + return await _seriesRepository.FindByPathAsync(path, cancellationToken); + } + public Series FindByTitle(string title, int year) { return _seriesRepository.FindByTitle(title.CleanSeriesTitle(), year); } + public async Task FindByTitleAsync(string title, int year, CancellationToken cancellationToken = default) + { + return await _seriesRepository.FindByTitleAsync(title.CleanSeriesTitle(), year, cancellationToken); + } + public void DeleteSeries(List seriesIds, bool deleteFiles, bool addImportListExclusion) { var series = _seriesRepository.Get(seriesIds).ToList(); @@ -167,37 +244,74 @@ public void DeleteSeries(List seriesIds, bool deleteFiles, bool addImportLi _eventAggregator.PublishEvent(new SeriesDeletedEvent(series, deleteFiles, addImportListExclusion)); } + public async Task DeleteSeriesAsync(List seriesIds, bool deleteFiles, bool addImportListExclusion, CancellationToken cancellationToken = default) + { + var series = await _seriesRepository.GetAsync(seriesIds, cancellationToken).ToListAsync(cancellationToken: cancellationToken); + await _seriesRepository.DeleteManyAsync(seriesIds, cancellationToken); + _eventAggregator.PublishEvent(new SeriesDeletedEvent(series, deleteFiles, addImportListExclusion)); + } + public List GetAllSeries() { return _seriesRepository.All().ToList(); } + public IAsyncEnumerable GetAllSeriesAsync(CancellationToken cancellationToken = default) + { + return _seriesRepository.AllAsync(cancellationToken); + } + public List AllSeriesTvdbIds() { return _seriesRepository.AllSeriesTvdbIds().ToList(); } + public async Task> AllSeriesTvdbIdsAsync(CancellationToken cancellation = default) + { + return await _seriesRepository.AllSeriesTvdbIdsAsync(cancellation); + } + public Dictionary GetAllSeriesPaths() { return _seriesRepository.AllSeriesPaths(); } + public async Task> GetAllSeriesPathsAsync(CancellationToken cancellationToken = default) + { + return await _seriesRepository.AllSeriesPathsAsync(cancellationToken); + } + public Dictionary> GetAllSeriesTags() { return _seriesRepository.AllSeriesTags(); } + public async Task>> GetAllSeriesTagsAsync(CancellationToken cancellationToken = default) + { + return await _seriesRepository.AllSeriesTagsAsync(cancellationToken); + } + public Dictionary GetAllSeriesQualityProfiles() { return _seriesRepository.AllSeriesQualityProfiles(); } + public async Task> GetAllSeriesQualityProfilesAsync(CancellationToken cancellationToken = default) + { + return await _seriesRepository.AllSeriesQualityProfilesAsync(cancellationToken); + } + public List AllForTag(int tagId) { return GetAllSeries().Where(s => s.Tags.Contains(tagId)) .ToList(); } + public IAsyncEnumerable AllForTagAsync(int tagId, CancellationToken cancellationToken = default) + { + return GetAllSeriesAsync(cancellationToken).Where(s => s.Tags.Contains(tagId)); + } + // updateEpisodesToMatchSeason is an override for EpisodeMonitoredService to use so a change via Season pass doesn't get nuked by the seasons loop. // TODO: Remove when seasons are split from series (or we come up with a better way to address this) public Series UpdateSeries(Series series, bool updateEpisodesToMatchSeason = true, bool publishUpdatedEvent = true) @@ -267,11 +381,23 @@ public bool SeriesPathExists(string folder) return _seriesRepository.SeriesPathExists(folder); } + public async Task SeriesPathExistsAsync(string folder, CancellationToken cancellationToken = default) + { + return await _seriesRepository.SeriesPathExistsAsync(folder, cancellationToken); + } + public void RemoveAddOptions(Series series) { _seriesRepository.SetFields(series, s => s.AddOptions); } + public async Task RemoveAddOptionsAsync(Series series, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + await _seriesRepository.SetFieldsAsync(series, s => s.AddOptions); + } + public bool UpdateTags(Series series) { _logger.Trace("Updating tags for {0}", series); diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseController.cs b/src/Sonarr.Api.V3/Indexers/ReleaseController.cs index 12da7f9ae..0aa320f32 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseController.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using FluentValidation; using Microsoft.AspNetCore.Mvc; @@ -68,7 +69,7 @@ public ReleaseController(IFetchAndParseRss rssFetcherAndParser, [HttpPost] [Consumes("application/json")] - public async Task DownloadRelease([FromBody] ReleaseResource release) + public async Task DownloadRelease([FromBody] ReleaseResource release, CancellationToken cancellationToken = default) { var remoteEpisode = _remoteEpisodeCache.Find(GetCacheKey(release)); @@ -105,7 +106,7 @@ public async Task DownloadRelease([FromBody] ReleaseResource release) ReleaseSource = remoteEpisode.ReleaseSource }; - remoteEpisode.Series = _seriesService.GetSeries(release.SeriesId!.Value); + remoteEpisode.Series = await _seriesService.GetSeriesAsync(release.SeriesId!.Value, cancellationToken); remoteEpisode.Episodes = _episodeService.GetEpisodes(release.EpisodeIds); remoteEpisode.ParsedEpisodeInfo.Quality = release.Quality; remoteEpisode.Languages = release.Languages; @@ -117,12 +118,12 @@ public async Task DownloadRelease([FromBody] ReleaseResource release) { var episode = _episodeService.GetEpisode(release.EpisodeId.Value); - remoteEpisode.Series = _seriesService.GetSeries(episode.SeriesId); + remoteEpisode.Series = await _seriesService.GetSeriesAsync(episode.SeriesId, cancellationToken); remoteEpisode.Episodes = new List { episode }; } else if (release.SeriesId.HasValue) { - var series = _seriesService.GetSeries(release.SeriesId.Value); + var series = await _seriesService.GetSeriesAsync(release.SeriesId.Value, cancellationToken); var episodes = _parsingService.GetEpisodes(remoteEpisode.ParsedEpisodeInfo, series, true); if (episodes.Empty()) diff --git a/src/Sonarr.Api.V5/Release/ReleaseController.cs b/src/Sonarr.Api.V5/Release/ReleaseController.cs index 968684419..df4bdaea7 100644 --- a/src/Sonarr.Api.V5/Release/ReleaseController.cs +++ b/src/Sonarr.Api.V5/Release/ReleaseController.cs @@ -85,7 +85,7 @@ protected override ReleaseResource GetResourceById(int id) [HttpPost] [Consumes("application/json")] - public async Task, NotFound>> DownloadRelease([FromBody] ReleaseGrabResource release) + public async Task, NotFound>> DownloadRelease([FromBody] ReleaseGrabResource release, CancellationToken cancellationToken = default) { var remoteEpisode = _remoteEpisodeCache.Find(GetCacheKey(release)); @@ -124,7 +124,7 @@ public async Task, NotFound>> DownloadRelease([F ReleaseSource = remoteEpisode.ReleaseSource }; - remoteEpisode.Series = _seriesService.GetSeries(overrideInfo.SeriesId!.Value); + remoteEpisode.Series = await _seriesService.GetSeriesAsync(overrideInfo.SeriesId!.Value, cancellationToken); remoteEpisode.Episodes = _episodeService.GetEpisodes(overrideInfo.EpisodeIds); remoteEpisode.ParsedEpisodeInfo.Quality = overrideInfo.Quality; remoteEpisode.Languages = overrideInfo.Languages; @@ -136,12 +136,12 @@ public async Task, NotFound>> DownloadRelease([F { var episode = _episodeService.GetEpisode(release.SearchInfo.EpisodeId.Value); - remoteEpisode.Series = _seriesService.GetSeries(episode.SeriesId); + remoteEpisode.Series = await _seriesService.GetSeriesAsync(episode.SeriesId, cancellationToken); remoteEpisode.Episodes = new List { episode }; } else if (release.SearchInfo?.SeriesId.HasValue == true) { - var series = _seriesService.GetSeries(release.SearchInfo.SeriesId.Value); + var series = await _seriesService.GetSeriesAsync(release.SearchInfo.SeriesId.Value, cancellationToken); var episodes = _parsingService.GetEpisodes(remoteEpisode.ParsedEpisodeInfo, series, true); if (episodes.Empty()) diff --git a/src/Sonarr.Api.V5/Series/SeriesController.cs b/src/Sonarr.Api.V5/Series/SeriesController.cs index 5eb93f035..2ea2605d1 100644 --- a/src/Sonarr.Api.V5/Series/SeriesController.cs +++ b/src/Sonarr.Api.V5/Series/SeriesController.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; +using System.Runtime.CompilerServices; using FluentValidation; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; @@ -109,27 +111,22 @@ public SeriesController(IBroadcastSignalRMessage signalRBroadcaster, [HttpGet] [Produces("application/json")] - public Ok> AllSeries(int? tvdbId, [FromQuery] SeriesSubresource[]? includeSubresources = null) + public async IAsyncEnumerable AllSeries(int? tvdbId, [FromQuery] SeriesSubresource[]? includeSubresources = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var seriesStats = _seriesStatisticsService.SeriesStatistics(); - var seriesResources = new List(); + var seriesStats = _seriesStatisticsService.SeriesStatistics().ToDictionary(x => x.SeriesId); var includeSeasonImages = includeSubresources.Contains(SeriesSubresource.SeasonImages); - if (tvdbId.HasValue) + await foreach (var series in FetchSeriesAsync(tvdbId, cancellationToken)) { - seriesResources.AddIfNotNull(_seriesService.FindByTvdbId(tvdbId.Value)?.ToResource(includeSeasonImages)); - } - else - { - seriesResources.AddRange(_seriesService.GetAllSeries().Select(s => s.ToResource(includeSeasonImages))); - } + var seriesResource = series.ToResource(includeSeasonImages); - MapCoversToLocal(seriesResources.ToArray()); - LinkSeriesStatistics(seriesResources, seriesStats.ToDictionary(x => x.SeriesId)); - PopulateAlternateTitles(seriesResources); - seriesResources.ForEach(LinkRootFolderPath); + MapCoversToLocal(seriesResource); + LinkSeriesStatistics(seriesResource, seriesStats.GetValueOrDefault(seriesResource.Id)); + PopulateAlternateTitles(seriesResource); + LinkRootFolderPath(seriesResource); - return TypedResults.Ok(seriesResources); + yield return seriesResource; + } } [NonAction] @@ -183,9 +180,9 @@ public Results, NotFound> GetResourceByIdWithErrorHandler(int [RestPostById] [Consumes("application/json")] [Produces("application/json")] - public Results, NotFound> AddSeries([FromBody] SeriesResource seriesResource) + public async Task, NotFound>> AddSeries([FromBody] SeriesResource seriesResource, CancellationToken cancellationToken = default) { - var series = _addSeriesService.AddSeries(seriesResource.ToModel()); + var series = await _addSeriesService.AddSeriesAsync(seriesResource.ToModel(), cancellationToken); return TypedCreated(series.Id); } @@ -193,9 +190,9 @@ public Results, NotFound> AddSeries([FromBody] SeriesRes [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public Results, NotFound> UpdateSeries([FromBody] SeriesResource seriesResource, [FromQuery] bool moveFiles = false) + public async Task, NotFound>> UpdateSeries([FromBody] SeriesResource seriesResource, [FromQuery] bool moveFiles = false, CancellationToken cancellationToken = default) { - var series = _seriesService.GetSeries(seriesResource.Id); + var series = await _seriesService.GetSeriesAsync(seriesResource.Id, cancellationToken); if (moveFiles) { @@ -246,9 +243,9 @@ public Results, NotFound> UpdateSeasonMonitored([FromRoute] i } [RestDeleteById] - public NoContent DeleteSeries(int id, bool deleteFiles = false, bool addImportListExclusion = false) + public async Task DeleteSeries(int id, bool deleteFiles = false, bool addImportListExclusion = false, CancellationToken cancellationToken = default) { - _seriesService.DeleteSeries(new List { id }, deleteFiles, addImportListExclusion); + await _seriesService.DeleteSeriesAsync([id], deleteFiles, addImportListExclusion, cancellationToken); return TypedResults.NoContent(); } @@ -269,6 +266,24 @@ public NoContent DeleteSeries(int id, bool deleteFiles = false, bool addImportLi return resource; } + private async IAsyncEnumerable FetchSeriesAsync(int? tvdbId, [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (tvdbId.HasValue) + { + if (await _seriesService.FindByTvdbIdAsync(tvdbId.Value, cancellationToken) is { } series) + { + yield return series; + } + } + else + { + await foreach (var series in _seriesService.GetAllSeriesAsync(cancellationToken)) + { + yield return series; + } + } + } + private void MapCoversToLocal(params SeriesResource[] series) { foreach (var seriesResource in series) @@ -293,8 +308,13 @@ private void LinkSeriesStatistics(List resources, Dictionary