added wanted list reads to sonarr and radarr

This commit is contained in:
Flaminel committed 2026-10-20 15:55:53 +03:00
1 parent 3f29613499
commit 00a1cdeda7
9 files changed
+286

No files matched your search

@@ -36,6 +36,16 @@ public abstract class ArrClient : IArrClient
/// </summary>
protected const int QueuePageSize = 200;
/// <summary>
/// Page size used when paging through an *arr instance's wanted lists.
/// </summary>
protected const int WantedPageSize = 1000;
/// <summary>
/// Hard cap on pages read from a wanted list, so a stuck pagination loop can't run forever.
/// </summary>
private const int MaxWantedPages = 1000;
/// <summary>
/// Command API path for this app's API version.
/// </summary>
@@ -308,6 +318,71 @@ public abstract class ArrClient : IArrClient
return await DeserializeStreamAsync<List<ArrHistoryRecord>>(response, cancellationToken) ?? [];
}
/// <summary>
/// Reads every record of a paged wanted list (missing or cutoff) from an arr instance.
/// </summary>
/// <param name="arrInstance">The arr instance to query.</param>
/// <param name="wantedKind">The wanted list to read, "missing" or "cutoff".</param>
/// <param name="monitored">Only monitored items when true, only unmonitored items when false.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected async Task<List<T>> GetAllWantedAsync<T>(
ArrInstance arrInstance,
string wantedKind,
bool monitored,
CancellationToken cancellationToken = default
)
{
List<T> records = [];
int totalRecords = 0;
int page = 1;
string monitoredValue = monitored ? "true" : "false";
do
{
UriBuilder uriBuilder = new(arrInstance.Url);
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/{ApiVersion}/wanted/{wantedKind}";
uriBuilder.Query = $"page={page}&pageSize={WantedPageSize}&monitored={monitoredValue}";
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
SetApiKey(request, arrInstance.ApiKey);
using HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
try
{
response.EnsureSuccessStatusCode();
}
catch
{
_logger.LogError("wanted {Kind} failed | {Uri}", wantedKind, uriBuilder.Uri);
throw;
}
ArrPagedResponse<T>? wantedResponse = await DeserializeStreamAsync<ArrPagedResponse<T>>(response, cancellationToken);
if (wantedResponse is null || wantedResponse.Records.Count is 0)
{
break;
}
if (totalRecords is 0)
{
totalRecords = wantedResponse.TotalRecords;
}
records.AddRange(wantedResponse.Records);
if (records.Count >= totalRecords)
{
break;
}
page++;
} while (page <= MaxWantedPages);
return records;
}
/// <inheritdoc/>
public virtual ManualImportFile? MapCandidate(QueueRecord record, ManualImportCandidate candidate) => null;
@@ -24,4 +24,14 @@ public interface IRadarrClient : IArrClient
/// Fetches the ids of movies Radarr grabbed since the given time.
/// </summary>
Task<HashSet<long>> GetRecentlyGrabbedMovieIdsAsync(ArrInstance arrInstance, DateTimeOffset since, CancellationToken cancellationToken = default);
/// <summary>
/// Fetches every movie Radarr lists as missing, filtered by monitored state.
/// </summary>
Task<List<SearchableMovie>> GetMissingMoviesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default);
/// <summary>
/// Fetches every movie Radarr lists as a cutoff-unmet upgrade, filtered by monitored state.
/// </summary>
Task<List<SearchableMovie>> GetCutoffUnmetMoviesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default);
}
@@ -34,4 +34,14 @@ public interface ISonarrClient : IArrClient
/// Fetches the series/season pairs Sonarr grabbed an episode for since the given time.
/// </summary>
Task<HashSet<(long SeriesId, long SeasonNumber)>> GetRecentlyGrabbedSeasonsAsync(ArrInstance arrInstance, DateTimeOffset since, CancellationToken cancellationToken = default);
/// <summary>
/// Fetches every episode Sonarr lists as missing, filtered by monitored state.
/// </summary>
Task<List<SearchableEpisode>> GetMissingEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default);
/// <summary>
/// Fetches every episode Sonarr lists as a cutoff-unmet upgrade, filtered by monitored state.
/// </summary>
Task<List<SearchableEpisode>> GetCutoffUnmetEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default);
}
@@ -234,6 +234,14 @@ public class RadarrClient : ArrClient, IRadarrClient
.ToHashSet();
}
/// <inheritdoc/>
public async Task<List<SearchableMovie>> GetMissingMoviesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
await GetAllWantedAsync<SearchableMovie>(arrInstance, "missing", monitored, cancellationToken);
/// <inheritdoc/>
public async Task<List<SearchableMovie>> GetCutoffUnmetMoviesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
await GetAllWantedAsync<SearchableMovie>(arrInstance, "cutoff", monitored, cancellationToken);
private async Task<Movie?> GetMovie(ArrInstance arrInstance, long movieId)
{
UriBuilder uriBuilder = new(arrInstance.Url);
@@ -350,6 +350,14 @@ public class SonarrClient : ArrClient, ISonarrClient
.ToHashSet();
}
/// <inheritdoc/>
public async Task<List<SearchableEpisode>> GetMissingEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
await GetAllWantedAsync<SearchableEpisode>(arrInstance, "missing", monitored, cancellationToken);
/// <inheritdoc/>
public async Task<List<SearchableEpisode>> GetCutoffUnmetEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
await GetAllWantedAsync<SearchableEpisode>(arrInstance, "cutoff", monitored, cancellationToken);
private async Task<List<Episode>?> GetEpisodesAsync(ArrInstance arrInstance, List<long> episodeIds)
{
UriBuilder uriBuilder = new(arrInstance.Url);