Add async overloaded methods for SeriesRepository

This commit is contained in:
Bogdan 2026-04-28 18:39:08 +03:00
parent 7daa508327
commit e80cd90614
8 changed files with 299 additions and 36 deletions

View file

@ -46,8 +46,8 @@ public void SetUp()
_xemEpisodes = new List<Episode>();
Mocker.GetMock<ISeriesService>()
.Setup(v => v.GetSeries(_xemSeries.Id))
.Returns(_xemSeries);
.Setup(v => v.GetSeriesAsync(_xemSeries.Id))
.ReturnsAsync(_xemSeries);
Mocker.GetMock<IEpisodeService>()
.Setup(v => v.GetEpisodesBySeason(_xemSeries.Id, It.IsAny<int>()))
@ -205,8 +205,8 @@ public async Task Tags_IndexerAndSeriesTagsMatch_IndexerIncluded()
.Build();
Mocker.GetMock<ISeriesService>()
.Setup(v => v.GetSeries(_xemSeries.Id))
.Returns(_xemSeries);
.Setup(v => v.GetSeriesAsync(_xemSeries.Id))
.ReturnsAsync(_xemSeries);
WithEpisodes();

View file

@ -58,7 +58,7 @@ public async Task<List<DownloadDecision>> EpisodeSearch(int episodeId, bool user
public async Task<List<DownloadDecision>> 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<List<DownloadDecision>> SeasonSearch(int seriesId, int seasonN
public async Task<List<DownloadDecision>> SeasonSearch(int seriesId, int seasonNumber, List<Episode> episodes, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch)
{
var series = _seriesService.GetSeries(seriesId);
var series = await _seriesService.GetSeriesAsync(seriesId);
if (series.SeriesType == SeriesTypes.Anime)
{

View file

@ -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<Series> AddSeriesAsync(Series newSeries, CancellationToken cancellationToken = default);
List<Series> AddSeries(List<Series> newSeries, bool ignoreErrors = false);
}
@ -54,6 +57,19 @@ public Series AddSeries(Series newSeries)
return newSeries;
}
public async Task<Series> 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<Series> AddSeries(List<Series> newSeries, bool ignoreErrors = false)
{
var added = DateTime.UtcNow;

View file

@ -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<Series>
{
bool SeriesPathExists(string path);
Task<bool> SeriesPathExistsAsync(string path, CancellationToken cancellationToken = default);
Series FindByTitle(string cleanTitle);
Task<Series> FindByTitleAsync(string cleanTitle, CancellationToken cancellationToken = default);
Series FindByTitle(string cleanTitle, int year);
Task<Series> FindByTitleAsync(string cleanTitle, int year, CancellationToken cancellationToken = default);
List<Series> FindByTitleInexact(string cleanTitle);
Task<List<Series>> FindByTitleInexactAsync(string cleanTitle, CancellationToken cancellationToken = default);
Series FindByTvdbId(int tvdbId);
Task<Series> FindByTvdbIdAsync(int tvdbId, CancellationToken cancellationToken = default);
Series FindByTvRageId(int tvRageId);
Task<Series> FindByTvRageIdAsync(int tvRageId, CancellationToken cancellationToken = default);
Series FindByImdbId(string imdbId);
Task<Series> FindByImdbIdAsync(string imdbId, CancellationToken cancellationToken = default);
Series FindByPath(string path);
Task<Series> FindByPathAsync(string path, CancellationToken cancellationToken = default);
List<int> AllSeriesTvdbIds();
Task<List<int>> AllSeriesTvdbIdsAsync(CancellationToken cancellationToken = default);
Dictionary<int, string> AllSeriesPaths();
Task<Dictionary<int, string>> AllSeriesPathsAsync(CancellationToken cancellationToken = default);
Dictionary<int, List<int>> AllSeriesTags();
Task<Dictionary<int, List<int>>> AllSeriesTagsAsync(CancellationToken cancellationToken = default);
Dictionary<int, int> AllSeriesQualityProfiles();
Task<Dictionary<int, int>> AllSeriesQualityProfilesAsync(CancellationToken cancellationToken = default);
}
public class SeriesRepository : BasicRepository<Series>, ISeriesRepository
@ -34,6 +48,11 @@ public bool SeriesPathExists(string path)
return Query(c => c.Path == path).Any();
}
public async Task<bool> 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<Series> 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<Series> 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<Series> FindByTitleInexact(string cleanTitle)
{
var builder = Builder().Where($"instr(@cleanTitle, \"Series\".\"CleanTitle\")", new { cleanTitle = cleanTitle });
@ -65,27 +102,59 @@ public List<Series> FindByTitleInexact(string cleanTitle)
return Query(builder).ToList();
}
public async Task<List<Series>> 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<Series> 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<Series> 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<Series> 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<Series> FindByPathAsync(string path, CancellationToken cancellationToken = default)
{
return await QueryAsync(s => s.Path == path, cancellationToken).SingleOrDefaultAsync(cancellationToken);
}
public List<int> AllSeriesTvdbIds()
{
using (var conn = _database.OpenConnection())
@ -94,6 +163,13 @@ public List<int> AllSeriesTvdbIds()
}
}
public async Task<List<int>> AllSeriesTvdbIdsAsync(CancellationToken cancellationToken = default)
{
await using var conn = await _database.OpenConnectionAsync(cancellationToken);
return await conn.QueryUnbufferedAsync<int>("SELECT \"TvdbId\" FROM \"Series\"").ToListAsync(cancellationToken);
}
public Dictionary<int, string> AllSeriesPaths()
{
using (var conn = _database.OpenConnection())
@ -103,6 +179,14 @@ public Dictionary<int, string> AllSeriesPaths()
}
}
public async Task<Dictionary<int, string>> AllSeriesPathsAsync(CancellationToken cancellationToken = default)
{
await using var conn = await _database.OpenConnectionAsync(cancellationToken);
return await conn.QueryUnbufferedAsync<KeyValuePair<int, string>>("SELECT \"Id\" AS Key, \"Path\" AS Value FROM \"Series\"")
.ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken: cancellationToken);
}
public Dictionary<int, List<int>> AllSeriesTags()
{
using (var conn = _database.OpenConnection())
@ -112,6 +196,14 @@ public Dictionary<int, List<int>> AllSeriesTags()
}
}
public async Task<Dictionary<int, List<int>>> AllSeriesTagsAsync(CancellationToken cancellationToken = default)
{
await using var conn = await _database.OpenConnectionAsync(cancellationToken);
return await conn.QueryUnbufferedAsync<KeyValuePair<int, List<int>>>("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<int, int> AllSeriesQualityProfiles()
{
using (var conn = _database.OpenConnection())
@ -121,6 +213,14 @@ public Dictionary<int, int> AllSeriesQualityProfiles()
}
}
public async Task<Dictionary<int, int>> AllSeriesQualityProfilesAsync(CancellationToken cancellationToken = default)
{
await using var conn = await _database.OpenConnectionAsync(cancellationToken);
return await conn.QueryUnbufferedAsync<KeyValuePair<int, int>>("SELECT \"Id\" AS Key, \"QualityProfileId\" AS Value FROM \"Series\"")
.ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken: cancellationToken);
}
private Series ReturnSingleSeriesOrThrow(List<Series> series)
{
if (series.Count == 0)

View file

@ -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<Series> GetSeriesAsync(int seriesId, CancellationToken cancellationToken = default);
List<Series> GetSeries(IEnumerable<int> seriesIds);
IAsyncEnumerable<Series> GetSeriesAsync(IEnumerable<int> seriesIds, CancellationToken cancellationToken = default);
Series AddSeries(Series newSeries);
Task<Series> AddSeriesAsync(Series newSeries, CancellationToken cancellationToken = default);
List<Series> AddSeries(List<Series> newSeries);
Task<List<Series>> AddSeriesAsync(List<Series> newSeries, CancellationToken cancellationToken = default);
Series FindByTvdbId(int tvdbId);
Task<Series> FindByTvdbIdAsync(int tvRageId, CancellationToken cancellationToken = default);
Series FindByTvRageId(int tvRageId);
Task<Series> FindByTvRageIdAsync(int tvRageId, CancellationToken cancellationToken = default);
Series FindByImdbId(string imdbId);
Task<Series> FindByImdbIdAsync(string imdbId, CancellationToken cancellationToken = default);
Series FindByTitle(string title);
Task<Series> FindByTitleAsync(string title, CancellationToken cancellationToken = default);
Series FindByTitle(string title, int year);
Task<Series> FindByTitleAsync(string title, int year, CancellationToken cancellationToken = default);
Series FindByTitleInexact(string title);
Series FindByPath(string path);
Task<Series> FindByPathAsync(string path, CancellationToken cancellationToken = default);
void DeleteSeries(List<int> seriesIds, bool deleteFiles, bool addImportListExclusion);
Task DeleteSeriesAsync(List<int> seriesIds, bool deleteFiles, bool addImportListExclusion, CancellationToken cancellationToken = default);
List<Series> GetAllSeries();
IAsyncEnumerable<Series> GetAllSeriesAsync(CancellationToken cancellationToken = default);
List<int> AllSeriesTvdbIds();
Task<List<int>> AllSeriesTvdbIdsAsync(CancellationToken cancellation = default);
Dictionary<int, string> GetAllSeriesPaths();
Task<Dictionary<int, string>> GetAllSeriesPathsAsync(CancellationToken cancellationToken = default);
Dictionary<int, List<int>> GetAllSeriesTags();
Task<Dictionary<int, List<int>>> GetAllSeriesTagsAsync(CancellationToken cancellationToken = default);
List<Series> AllForTag(int tagId);
IAsyncEnumerable<Series> AllForTagAsync(int tagId, CancellationToken cancellationToken = default);
Dictionary<int, int> GetAllSeriesQualityProfiles();
Task<Dictionary<int, int>> GetAllSeriesQualityProfilesAsync(CancellationToken cancellationToken = default);
Series UpdateSeries(Series series, bool updateEpisodesToMatchSeason = true, bool publishUpdatedEvent = true);
List<Series> UpdateSeries(List<Series> series, bool useExistingRelativeFolder);
bool SeriesPathExists(string folder);
Task<bool> 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<Series> GetSeriesAsync(int seriesId, CancellationToken cancellationToken = default)
{
return await _seriesRepository.GetAsync(seriesId, cancellationToken);
}
public List<Series> GetSeries(IEnumerable<int> seriesIds)
{
return _seriesRepository.Get(seriesIds).ToList();
}
public IAsyncEnumerable<Series> GetSeriesAsync(IEnumerable<int> 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<Series> AddSeriesAsync(Series newSeries, CancellationToken cancellationToken = default)
{
await _seriesRepository.InsertAsync(newSeries, cancellationToken);
_eventAggregator.PublishEvent(new SeriesAddedEvent(await GetSeriesAsync(newSeries.Id, cancellationToken)));
return newSeries;
}
public List<Series> AddSeries(List<Series> newSeries)
{
_seriesRepository.InsertMany(newSeries);
@ -86,26 +125,54 @@ public List<Series> AddSeries(List<Series> newSeries)
return newSeries;
}
public async Task<List<Series>> AddSeriesAsync(List<Series> 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<Series> FindByTvdbIdAsync(int tvRageId, CancellationToken cancellationToken = default)
{
return await _seriesRepository.FindByTvdbIdAsync(tvRageId, cancellationToken);
}
public Series FindByTvRageId(int tvRageId)
{
return _seriesRepository.FindByTvRageId(tvRageId);
}
public async Task<Series> FindByTvRageIdAsync(int tvRageId, CancellationToken cancellationToken = default)
{
return await _seriesRepository.FindByTvRageIdAsync(tvRageId, cancellationToken);
}
public Series FindByImdbId(string imdbId)
{
return _seriesRepository.FindByImdbId(imdbId);
}
public async Task<Series> 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<Series> 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<Series> 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<Series> FindByTitleAsync(string title, int year, CancellationToken cancellationToken = default)
{
return await _seriesRepository.FindByTitleAsync(title.CleanSeriesTitle(), year, cancellationToken);
}
public void DeleteSeries(List<int> seriesIds, bool deleteFiles, bool addImportListExclusion)
{
var series = _seriesRepository.Get(seriesIds).ToList();
@ -167,37 +244,74 @@ public void DeleteSeries(List<int> seriesIds, bool deleteFiles, bool addImportLi
_eventAggregator.PublishEvent(new SeriesDeletedEvent(series, deleteFiles, addImportListExclusion));
}
public async Task DeleteSeriesAsync(List<int> 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<Series> GetAllSeries()
{
return _seriesRepository.All().ToList();
}
public IAsyncEnumerable<Series> GetAllSeriesAsync(CancellationToken cancellationToken = default)
{
return _seriesRepository.AllAsync(cancellationToken);
}
public List<int> AllSeriesTvdbIds()
{
return _seriesRepository.AllSeriesTvdbIds().ToList();
}
public async Task<List<int>> AllSeriesTvdbIdsAsync(CancellationToken cancellation = default)
{
return await _seriesRepository.AllSeriesTvdbIdsAsync(cancellation);
}
public Dictionary<int, string> GetAllSeriesPaths()
{
return _seriesRepository.AllSeriesPaths();
}
public async Task<Dictionary<int, string>> GetAllSeriesPathsAsync(CancellationToken cancellationToken = default)
{
return await _seriesRepository.AllSeriesPathsAsync(cancellationToken);
}
public Dictionary<int, List<int>> GetAllSeriesTags()
{
return _seriesRepository.AllSeriesTags();
}
public async Task<Dictionary<int, List<int>>> GetAllSeriesTagsAsync(CancellationToken cancellationToken = default)
{
return await _seriesRepository.AllSeriesTagsAsync(cancellationToken);
}
public Dictionary<int, int> GetAllSeriesQualityProfiles()
{
return _seriesRepository.AllSeriesQualityProfiles();
}
public async Task<Dictionary<int, int>> GetAllSeriesQualityProfilesAsync(CancellationToken cancellationToken = default)
{
return await _seriesRepository.AllSeriesQualityProfilesAsync(cancellationToken);
}
public List<Series> AllForTag(int tagId)
{
return GetAllSeries().Where(s => s.Tags.Contains(tagId))
.ToList();
}
public IAsyncEnumerable<Series> 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<bool> 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);

View file

@ -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<object> DownloadRelease([FromBody] ReleaseResource release)
public async Task<object> DownloadRelease([FromBody] ReleaseResource release, CancellationToken cancellationToken = default)
{
var remoteEpisode = _remoteEpisodeCache.Find(GetCacheKey(release));
@ -105,7 +106,7 @@ public async Task<object> 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<object> 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> { 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())

View file

@ -85,7 +85,7 @@ protected override ReleaseResource GetResourceById(int id)
[HttpPost]
[Consumes("application/json")]
public async Task<Results<Ok<ReleaseGrabResource>, NotFound>> DownloadRelease([FromBody] ReleaseGrabResource release)
public async Task<Results<Ok<ReleaseGrabResource>, NotFound>> DownloadRelease([FromBody] ReleaseGrabResource release, CancellationToken cancellationToken = default)
{
var remoteEpisode = _remoteEpisodeCache.Find(GetCacheKey(release));
@ -124,7 +124,7 @@ public async Task<Results<Ok<ReleaseGrabResource>, 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<Results<Ok<ReleaseGrabResource>, 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> { 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())

View file

@ -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<List<SeriesResource>> AllSeries(int? tvdbId, [FromQuery] SeriesSubresource[]? includeSubresources = null)
public async IAsyncEnumerable<SeriesResource> AllSeries(int? tvdbId, [FromQuery] SeriesSubresource[]? includeSubresources = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var seriesStats = _seriesStatisticsService.SeriesStatistics();
var seriesResources = new List<SeriesResource>();
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<Ok<SeriesResource>, NotFound> GetResourceByIdWithErrorHandler(int
[RestPostById]
[Consumes("application/json")]
[Produces("application/json")]
public Results<Created<SeriesResource>, NotFound> AddSeries([FromBody] SeriesResource seriesResource)
public async Task<Results<Created<SeriesResource>, 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<Created<SeriesResource>, NotFound> AddSeries([FromBody] SeriesRes
[RestPutById]
[Consumes("application/json")]
[Produces("application/json")]
public Results<Accepted<SeriesResource>, NotFound> UpdateSeries([FromBody] SeriesResource seriesResource, [FromQuery] bool moveFiles = false)
public async Task<Results<Accepted<SeriesResource>, 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<Ok<SeasonResource>, NotFound> UpdateSeasonMonitored([FromRoute] i
}
[RestDeleteById]
public NoContent DeleteSeries(int id, bool deleteFiles = false, bool addImportListExclusion = false)
public async Task<NoContent> DeleteSeries(int id, bool deleteFiles = false, bool addImportListExclusion = false, CancellationToken cancellationToken = default)
{
_seriesService.DeleteSeries(new List<int> { 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<NzbDrone.Core.Tv.Series> 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<SeriesResource> resources, Dictionary<int
}
}
private void LinkSeriesStatistics(SeriesResource resource, SeriesStatistics seriesStatistics)
private void LinkSeriesStatistics(SeriesResource resource, SeriesStatistics? seriesStatistics)
{
if (seriesStatistics == null)
{
return;
}
// Only set last aired from statistics if it's missing from the series itself
resource.LastAired ??= seriesStatistics.LastAired;