Add v5 manual import endpoints

This commit is contained in:
Mark McDowall 2025-12-20 21:39:56 -08:00
parent 4c13a01c8e
commit 8da611ea58
3 changed files with 203 additions and 0 deletions

View file

@ -0,0 +1,82 @@
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles.EpisodeImport.Manual;
using NzbDrone.Core.Qualities;
using Sonarr.Http;
using Sonarr.Http.REST;
namespace Sonarr.Api.V5.ManualImport;
[V5ApiController]
public class ManualImportController : Controller
{
private readonly IManualImportService _manualImportService;
public ManualImportController(IManualImportService manualImportService)
{
_manualImportService = manualImportService;
}
[HttpGet]
[Produces("application/json")]
public List<ManualImportResource> GetMediaFiles(string? folder, string? downloadId, int? seriesId, int? seasonNumber, bool filterExistingFiles = true)
{
if (seriesId.HasValue && downloadId.IsNullOrWhiteSpace())
{
return _manualImportService.GetMediaFiles(seriesId.Value, seasonNumber).ToResource().Select(AddQualityWeight).ToList();
}
return _manualImportService.GetMediaFiles(folder, downloadId, seriesId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList();
}
[HttpPost]
[Consumes("application/json")]
public List<ManualImportResource> ReprocessItems([FromBody] List<ManualImportReprocessResource> items)
{
if (items is { Count: 0 })
{
throw new BadRequestException("items must be provided");
}
var updatedItems = new List<ManualImportItem>();
foreach (var item in items)
{
var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List<int>(), item.ReleaseGroup, item.Quality, item.Languages, item.IndexerFlags, item.ReleaseType);
// Only use the processed item's languages, quality, and release group if the user hasn't specified them.
// Languages won't be returned when reprocessing if the season/episode isn't filled in yet and we don't want to return no languages to the client.
if (processedItem.Languages.Empty() || item.Languages.Count > 1 || (item.Languages.SingleOrDefault() ?? Language.Unknown) == Language.Unknown)
{
processedItem.Languages = item.Languages;
}
if (item.Quality?.Quality != Quality.Unknown)
{
processedItem.Quality = item.Quality;
}
if (item.ReleaseGroup.IsNotNullOrWhiteSpace())
{
processedItem.ReleaseGroup = item.ReleaseGroup;
}
updatedItems.Add(processedItem);
}
return updatedItems.ToResource();
}
private ManualImportResource AddQualityWeight(ManualImportResource item)
{
if (item.Quality != null)
{
item.QualityWeight = Quality.DefaultQualityDefinitions.Single(q => q.Quality == item.Quality.Quality).Weight;
item.QualityWeight += item.Quality.Revision.Real * 10;
item.QualityWeight += item.Quality.Revision.Version;
}
return item;
}
}

View file

@ -0,0 +1,26 @@
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using Sonarr.Api.V5.CustomFormats;
using Sonarr.Api.V5.Episodes;
using Sonarr.Http.REST;
namespace Sonarr.Api.V5.ManualImport;
public class ManualImportReprocessResource : RestResource
{
public string? Path { get; set; }
public int SeriesId { get; set; }
public int? SeasonNumber { get; set; }
public List<EpisodeResource> Episodes { get; set; } = [];
public List<int>? EpisodeIds { get; set; }
public QualityModel? Quality { get; set; }
public List<Language> Languages { get; set; } = [];
public string? ReleaseGroup { get; set; }
public string? DownloadId { get; set; }
public List<CustomFormatResource> CustomFormats { get; set; } = [];
public int CustomFormatScore { get; set; }
public int IndexerFlags { get; set; }
public ReleaseType ReleaseType { get; set; }
public IEnumerable<ImportRejectionResource> Rejections { get; set; } = [];
}

View file

@ -0,0 +1,95 @@
using NzbDrone.Common.Crypto;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.MediaFiles.EpisodeImport.Manual;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using Sonarr.Api.V5.CustomFormats;
using Sonarr.Api.V5.Episodes;
using Sonarr.Api.V5.Series;
using Sonarr.Http.REST;
namespace Sonarr.Api.V5.ManualImport;
public class ManualImportResource : RestResource
{
public string? Path { get; set; }
public string? RelativePath { get; set; }
public string? FolderName { get; set; }
public string? Name { get; set; }
public long Size { get; set; }
public SeriesResource? Series { get; set; }
public int? SeasonNumber { get; set; }
public List<EpisodeResource> Episodes { get; set; } = [];
public int? EpisodeFileId { get; set; }
public string? ReleaseGroup { get; set; }
public QualityModel? Quality { get; set; }
public List<Language> Languages { get; set; } = [];
public int QualityWeight { get; set; }
public string? DownloadId { get; set; }
public List<CustomFormatResource> CustomFormats { get; set; } = [];
public int CustomFormatScore { get; set; }
public int IndexerFlags { get; set; }
public ReleaseType ReleaseType { get; set; }
public IEnumerable<ImportRejectionResource> Rejections { get; set; } = [];
}
public static class ManualImportResourceMapper
{
public static ManualImportResource ToResource(this ManualImportItem model)
{
var customFormats = model.CustomFormats;
var customFormatScore = model.Series?.QualityProfile?.Value?.CalculateCustomFormatScore(customFormats) ?? 0;
return new ManualImportResource
{
Id = HashConverter.GetHashInt31(model.Path),
Path = model.Path,
RelativePath = model.RelativePath,
FolderName = model.FolderName,
Name = model.Name,
Size = model.Size,
Series = model.Series?.ToResource(),
SeasonNumber = model.SeasonNumber,
Episodes = model.Episodes.ToResource(),
EpisodeFileId = model.EpisodeFileId,
ReleaseGroup = model.ReleaseGroup,
Quality = model.Quality,
Languages = model.Languages,
CustomFormats = customFormats.ToResource(false),
CustomFormatScore = customFormatScore,
// QualityWeight
DownloadId = model.DownloadId,
IndexerFlags = model.IndexerFlags,
ReleaseType = model.ReleaseType,
Rejections = model.Rejections.Select(r => r.ToResource())
};
}
public static List<ManualImportResource> ToResource(this IEnumerable<ManualImportItem> models)
{
return models.Select(ToResource).ToList();
}
}
public class ImportRejectionResource
{
public ImportRejectionReason Reason { get; set; }
public string? Message { get; set; }
public RejectionType Type { get; set; }
}
public static class ImportRejectionResourceMapper
{
public static ImportRejectionResource ToResource(this ImportRejection rejection)
{
return new ImportRejectionResource
{
Reason = rejection.Reason,
Message = rejection.Message,
Type = rejection.Type
};
}
}