New: v4 API (DROP v3 AFTER TESTING PERIOD)

zeus-oidc
Qstick 2 years ago
parent 583c5d501c
commit c3368d9e6c

@ -1093,7 +1093,7 @@ stages:
projectVersion: '$(radarrVersion)'
extraProperties: |
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
sonar.coverage.exclusions=**/Radarr.Api.V3/**/*
sonar.coverage.exclusions=**/Radarr.Api.V*/**/*
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: |

@ -29,7 +29,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
dotnet new tool-manifest
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
dotnet tool run swagger tofile --output ./src/Radarr.Api.V4/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v4 &
sleep 45

@ -22,7 +22,7 @@ function getUrls(state) {
tags
} = state;
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v3/calendar/Radarr.ics?`;
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v4/calendar/Radarr.ics?`;
if (unmonitored) {
icalUrl += 'unmonitored=true&';

@ -38,6 +38,7 @@ namespace NzbDrone.Host
"Radarr.Core",
"Radarr.SignalR",
"Radarr.Api.V3",
"Radarr.Api.V4",
"Radarr.Http"
};

@ -15,6 +15,7 @@
<ProjectReference Include="..\NzbDrone.Core\Radarr.Core.csproj" />
<ProjectReference Include="..\NzbDrone.SignalR\Radarr.SignalR.csproj" />
<ProjectReference Include="..\Radarr.Api.V3\Radarr.Api.V3.csproj" />
<ProjectReference Include="..\Radarr.Api.V4\Radarr.Api.V4.csproj" />
<ProjectReference Include="..\Radarr.Http\Radarr.Http.csproj" />
</ItemGroup>
<ItemGroup>

@ -24,7 +24,7 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Host.AccessControl;
using NzbDrone.Http.Authentication;
using NzbDrone.SignalR;
using Radarr.Api.V3.System;
using Radarr.Api.V4.System;
using Radarr.Http;
using Radarr.Http.Authentication;
using Radarr.Http.ErrorManagement;
@ -87,6 +87,7 @@ namespace NzbDrone.Host
options.ReturnHttpNotAcceptable = true;
})
.AddApplicationPart(typeof(SystemController).Assembly)
.AddApplicationPart(typeof(Radarr.Api.V3.System.SystemController).Assembly)
.AddApplicationPart(typeof(StaticResourceController).Assembly)
.AddJsonOptions(options =>
{
@ -96,9 +97,9 @@ namespace NzbDrone.Host
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v3", new OpenApiInfo
c.SwaggerDoc("v4", new OpenApiInfo
{
Version = "3.0.0",
Version = "4.0.0",
Title = "Radarr",
Description = "Radarr API docs",
License = new OpenApiLicense
@ -274,6 +275,7 @@ namespace NzbDrone.Host
app.UseMiddleware<CacheHeaderMiddleware>();
app.UseMiddleware<IfModifiedMiddleware>();
app.UseMiddleware<BufferingMiddleware>(new List<string> { "/api/v3/command" });
app.UseMiddleware<BufferingMiddleware>(new List<string> { "/api/v4/command" });
app.UseWebSockets();

@ -1,6 +1,6 @@
using FluentAssertions;
using NUnit.Framework;
using Radarr.Api.V3.Movies;
using Radarr.Api.V4.Movies;
namespace NzbDrone.Integration.Test.ApiTests
{
@ -15,7 +15,7 @@ namespace NzbDrone.Integration.Test.ApiTests
{
_movie = EnsureMovie(11, "The Blocklist");
Blocklist.Post(new Radarr.Api.V3.Blocklist.BlocklistResource
Blocklist.Post(new Radarr.Api.V4.Blocklist.BlocklistResource
{
MovieId = _movie.Id,
SourceTitle = "Blocklist.S01E01.Brought.To.You.By-BoomBoxHD"

@ -4,7 +4,7 @@ using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Integration.Test.Client;
using Radarr.Api.V3.Movies;
using Radarr.Api.V4.Movies;
namespace NzbDrone.Integration.Test.ApiTests
{

@ -2,7 +2,7 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Integration.Test.Client;
using Radarr.Api.V3.DiskSpace;
using Radarr.Api.V4.DiskSpace;
namespace NzbDrone.Integration.Test.ApiTests
{

@ -3,7 +3,7 @@ using FluentAssertions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using NzbDrone.Core.ThingiProvider;
using Radarr.Api.V3.Indexers;
using Radarr.Api.V4.Indexers;
using Radarr.Http.ClientSchema;
namespace NzbDrone.Integration.Test.ApiTests

@ -3,7 +3,7 @@ using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Test.Common;
using Radarr.Api.V3.Movies;
using Radarr.Api.V4.Movies;
namespace NzbDrone.Integration.Test.ApiTests
{

@ -2,7 +2,7 @@ using System.Linq;
using System.Net;
using FluentAssertions;
using NUnit.Framework;
using Radarr.Api.V3.Indexers;
using Radarr.Api.V4.Indexers;
namespace NzbDrone.Integration.Test.ApiTests
{

@ -1,7 +1,7 @@
using System;
using FluentAssertions;
using NUnit.Framework;
using Radarr.Api.V3.RootFolders;
using Radarr.Api.V4.RootFolders;
namespace NzbDrone.Integration.Test.ApiTests
{

@ -1,7 +1,7 @@
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using Radarr.Api.V3.Tags;
using Radarr.Api.V4.Tags;
namespace NzbDrone.Integration.Test.ApiTests
{

@ -1,5 +1,5 @@
using System.Collections.Generic;
using Radarr.Api.V3.DownloadClient;
using Radarr.Api.V4.DownloadClient;
using RestSharp;
namespace NzbDrone.Integration.Test.Client

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using Radarr.Api.V3.Indexers;
using Radarr.Api.V4.Indexers;
using RestSharp;
namespace NzbDrone.Integration.Test.Client

@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Net;
using Radarr.Api.V3.Movies;
using Radarr.Api.V4.Movies;
using RestSharp;
namespace NzbDrone.Integration.Test.Client

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Net;
using Radarr.Api.V3.MovieFiles;
using Radarr.Api.V3.Movies;
using Radarr.Api.V4.MovieFiles;
using Radarr.Api.V4.Movies;
using RestSharp;
namespace NzbDrone.Integration.Test.Client

@ -1,5 +1,5 @@
using System.Collections.Generic;
using Radarr.Api.V3.Notifications;
using Radarr.Api.V4.Notifications;
using RestSharp;
namespace NzbDrone.Integration.Test.Client

@ -1,4 +1,4 @@
using Radarr.Api.V3.Indexers;
using Radarr.Api.V4.Indexers;
using RestSharp;
namespace NzbDrone.Integration.Test.Client

@ -20,15 +20,15 @@ namespace NzbDrone.Integration.Test
var logFile = "radarr.trace.txt";
var logLines = Logs.GetLogFileLines(logFile);
var resultPost = Movies.InvalidPost(new Radarr.Api.V3.Movies.MovieResource());
var resultPost = Movies.InvalidPost(new Radarr.Api.V4.Movies.MovieResource());
// Skip 2 and 1 to ignore the logs endpoint
logLines = Logs.GetLogFileLines(logFile).Skip(logLines.Length + 2).ToArray();
Array.Resize(ref logLines, logLines.Length - 1);
logLines.Should().Contain(v => v.Contains("|Trace|Http|Req") && v.Contains("/api/v3/movie/"));
logLines.Should().Contain(v => v.Contains("|Trace|Http|Res") && v.Contains("/api/v3/movie/: 400.BadRequest"));
logLines.Should().Contain(v => v.Contains("|Debug|Api|") && v.Contains("/api/v3/movie/: 400.BadRequest"));
logLines.Should().Contain(v => v.Contains("|Trace|Http|Req") && v.Contains("/api/v4/movie/"));
logLines.Should().Contain(v => v.Contains("|Trace|Http|Res") && v.Contains("/api/v4/movie/: 400.BadRequest"));
logLines.Should().Contain(v => v.Contains("|Debug|Api|") && v.Contains("/api/v4/movie/: 400.BadRequest"));
}
}
}

@ -53,7 +53,7 @@ namespace NzbDrone.Integration.Test
// Make sure tasks have been initialized so the config put below doesn't cause errors
WaitForCompletion(() => Tasks.All().SelectList(x => x.TaskName).Contains("RssSync"));
Indexers.Post(new Radarr.Api.V3.Indexers.IndexerResource
Indexers.Post(new Radarr.Api.V4.Indexers.IndexerResource
{
EnableRss = false,
EnableInteractiveSearch = false,

@ -17,16 +17,16 @@ using NzbDrone.Core.Qualities;
using NzbDrone.Integration.Test.Client;
using NzbDrone.SignalR;
using NzbDrone.Test.Common.Categories;
using Radarr.Api.V3.Blocklist;
using Radarr.Api.V3.Config;
using Radarr.Api.V3.DownloadClient;
using Radarr.Api.V3.History;
using Radarr.Api.V3.MovieFiles;
using Radarr.Api.V3.Movies;
using Radarr.Api.V3.Profiles.Quality;
using Radarr.Api.V3.RootFolders;
using Radarr.Api.V3.System.Tasks;
using Radarr.Api.V3.Tags;
using Radarr.Api.V4.Blocklist;
using Radarr.Api.V4.Config;
using Radarr.Api.V4.DownloadClient;
using Radarr.Api.V4.History;
using Radarr.Api.V4.MovieFiles;
using Radarr.Api.V4.Movies;
using Radarr.Api.V4.Profiles.Quality;
using Radarr.Api.V4.RootFolders;
using Radarr.Api.V4.System.Tasks;
using Radarr.Api.V4.Tags;
using RestSharp;
namespace NzbDrone.Integration.Test
@ -95,7 +95,7 @@ namespace NzbDrone.Integration.Test
protected virtual void InitRestClients()
{
RestClient = new RestClient(RootUrl + "api/v3/");
RestClient = new RestClient(RootUrl + "api/v4/");
RestClient.AddDefaultHeader("Authentication", ApiKey);
RestClient.AddDefaultHeader("X-Api-Key", ApiKey);

@ -8,6 +8,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" />
<ProjectReference Include="..\Radarr.Api.V3\Radarr.Api.V3.csproj" />
<ProjectReference Include="..\Radarr.Api.V4\Radarr.Api.V4.csproj" />
</ItemGroup>
</Project>

@ -31,7 +31,7 @@ namespace NzbDrone.Test.Common
public NzbDroneRunner(Logger logger, PostgresOptions postgresOptions, int port = 7878)
{
_processProvider = new ProcessProvider(logger);
_restClient = new RestClient($"http://localhost:{port}/api/v3");
_restClient = new RestClient($"http://localhost:{port}/api/v4");
PostgresOptions = postgresOptions;
Port = port;

@ -82,7 +82,7 @@ namespace Radarr.Api.V3.Movies
_commandQueueManager = commandQueueManager;
_logger = logger;
SharedValidator.RuleFor(s => s.QualityProfileId).ValidId().When(s => s.QualityProfileIds == null || s.QualityProfileIds.Empty());
SharedValidator.RuleFor(s => s.QualityProfileId).ValidId();
SharedValidator.RuleFor(s => s.Path)
.Cascade(CascadeMode.StopOnFirstFailure)
@ -95,9 +95,6 @@ namespace Radarr.Api.V3.Movies
.SetValidator(systemFolderValidator)
.When(s => !s.Path.IsNullOrWhiteSpace());
SharedValidator.RuleFor(s => s.QualityProfileIds).NotNull().When(s => s.QualityProfileId == 0);
SharedValidator.RuleForEach(s => s.QualityProfileIds).SetValidator(profileExistsValidator);
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.RootFolderPath)
.IsValidPath()
@ -234,6 +231,8 @@ namespace Radarr.Api.V3.Movies
private void LinkMovieStatistics(MovieResource resource, MovieStatistics seriesStatistics)
{
resource.Statistics = seriesStatistics.ToResource();
resource.SizeOnDisk = seriesStatistics.SizeOnDisk;
resource.HasFile = seriesStatistics.MovieFileCount > 0;
}
[RestPostById]

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Policy;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaCover;
@ -18,7 +19,6 @@ namespace Radarr.Api.V3.Movies
{
Monitored = true;
MinimumAvailability = MovieStatusType.Released;
QualityProfileIds = new List<int>();
}
// Todo: Sorters should be done completely on the client
@ -51,10 +51,10 @@ namespace Radarr.Api.V3.Movies
// View & Edit
public string Path { get; set; }
public List<int> QualityProfileIds { get; set; }
// Compatabilitiy
public int QualityProfileId { get; set; }
public bool HasFile { get; set; }
// Editing Only
public bool Monitored { get; set; }
@ -115,7 +115,6 @@ namespace Radarr.Api.V3.Movies
SecondaryYear = model.MovieMetadata.Value.SecondaryYear,
Path = model.Path,
QualityProfileIds = model.QualityProfileIds,
QualityProfileId = model.QualityProfileIds.FirstOrDefault(),
Monitored = model.Monitored,
@ -151,13 +150,6 @@ namespace Radarr.Api.V3.Movies
return null;
}
var profiles = resource.QualityProfileIds;
if (resource.QualityProfileIds.Count == 0)
{
profiles.Add(resource.QualityProfileId);
}
return new Movie
{
Id = resource.Id,
@ -186,7 +178,7 @@ namespace Radarr.Api.V3.Movies
},
Path = resource.Path,
QualityProfileIds = resource.QualityProfileIds,
QualityProfileIds = new List<int> { resource.QualityProfileId },
Monitored = resource.Monitored,
MinimumAvailability = resource.MinimumAvailability,

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Radarr.Api.V4.Blocklist
{
public class BlocklistBulkResource
{
public List<int> Ids { get; set; }
}
}

@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Datastore;
using Radarr.Http;
using Radarr.Http.Extensions;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V4.Blocklist
{
[V4ApiController]
public class BlocklistController : Controller
{
private readonly IBlocklistService _blocklistService;
private readonly ICustomFormatCalculationService _formatCalculator;
public BlocklistController(IBlocklistService blocklistService,
ICustomFormatCalculationService formatCalculator)
{
_blocklistService = blocklistService;
_formatCalculator = formatCalculator;
}
[HttpGet]
public PagingResource<BlocklistResource> GetBlocklist()
{
var pagingResource = Request.ReadPagingResourceFromRequest<BlocklistResource>();
var pagingSpec = pagingResource.MapToPagingSpec<BlocklistResource, NzbDrone.Core.Blocklisting.Blocklist>("date", SortDirection.Descending);
return pagingSpec.ApplyToPage(_blocklistService.Paged, model => BlocklistResourceMapper.MapToResource(model, _formatCalculator));
}
[HttpGet("movie")]
public List<BlocklistResource> GetMovieBlocklist(int movieId)
{
return _blocklistService.GetByMovieId(movieId).Select(h => BlocklistResourceMapper.MapToResource(h, _formatCalculator)).ToList();
}
[RestDeleteById]
public void DeleteBlocklist(int id)
{
_blocklistService.Delete(id);
}
[HttpDelete("bulk")]
public object Remove([FromBody] BlocklistBulkResource resource)
{
_blocklistService.Delete(resource.Ids);
return new { };
}
}
}

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Qualities;
using Radarr.Api.V4.CustomFormats;
using Radarr.Api.V4.Movies;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Blocklist
{
public class BlocklistResource : RestResource
{
public int MovieId { get; set; }
public string SourceTitle { get; set; }
public List<Language> Languages { get; set; }
public QualityModel Quality { get; set; }
public List<CustomFormatResource> CustomFormats { get; set; }
public DateTime Date { get; set; }
public DownloadProtocol Protocol { get; set; }
public string Indexer { get; set; }
public string Message { get; set; }
public MovieResource Movie { get; set; }
}
public static class BlocklistResourceMapper
{
public static BlocklistResource MapToResource(this NzbDrone.Core.Blocklisting.Blocklist model, ICustomFormatCalculationService formatCalculator)
{
if (model == null)
{
return null;
}
return new BlocklistResource
{
Id = model.Id,
MovieId = model.MovieId,
SourceTitle = model.SourceTitle,
Languages = model.Languages,
Quality = model.Quality,
CustomFormats = formatCalculator.ParseCustomFormat(model).ToResource(),
Date = model.Date,
Protocol = model.Protocol,
Indexer = model.Indexer,
Message = model.Message,
Movie = model.Movie.ToResource(0)
};
}
}
}

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.SignalR;
using Radarr.Api.V4.Movies;
using Radarr.Http;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Calendar
{
[V4ApiController]
public class CalendarController : RestControllerWithSignalR<MovieResource, Movie>
{
private readonly IMovieService _moviesService;
private readonly IMovieTranslationService _movieTranslationService;
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
private readonly IConfigService _configService;
public CalendarController(IBroadcastSignalRMessage signalR,
IMovieService moviesService,
IMovieTranslationService movieTranslationService,
IUpgradableSpecification qualityUpgradableSpecification,
IConfigService configService)
: base(signalR)
{
_moviesService = moviesService;
_movieTranslationService = movieTranslationService;
_qualityUpgradableSpecification = qualityUpgradableSpecification;
_configService = configService;
}
protected override MovieResource GetResourceById(int id)
{
throw new NotImplementedException();
}
[HttpGet]
public List<MovieResource> GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeArtist = false)
{
var startUse = start ?? DateTime.Today;
var endUse = end ?? DateTime.Today.AddDays(2);
var resources = _moviesService.GetMoviesBetweenDates(startUse, endUse, unmonitored).Select(MapToResource);
return resources.OrderBy(e => e.InCinemas).ToList();
}
protected MovieResource MapToResource(Movie movie)
{
if (movie == null)
{
return null;
}
var availDelay = _configService.AvailabilityDelay;
var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.Id);
var translation = GetMovieTranslation(translations, movie.MovieMetadata);
var resource = movie.ToResource(availDelay, translation, _qualityUpgradableSpecification);
return resource;
}
private MovieTranslation GetMovieTranslation(List<MovieTranslation> translations, MovieMetadata movie)
{
if ((Language)_configService.MovieInfoLanguage == Language.Original)
{
return new MovieTranslation
{
Title = movie.OriginalTitle,
Overview = movie.Overview
};
}
return translations.FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage && t.MovieMetadataId == movie.Id);
}
}
}

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Ical.Net;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Tags;
using Radarr.Http;
namespace Radarr.Api.V4.Calendar
{
[V4FeedController("calendar")]
public class CalendarFeedController : Controller
{
private readonly IMovieService _movieService;
private readonly ITagService _tagService;
public CalendarFeedController(IMovieService movieService, ITagService tagService)
{
_movieService = movieService;
_tagService = tagService;
}
[HttpGet("Radarr.ics")]
public IActionResult GetCalendarFeed(int pastDays = 7, int futureDays = 28, string tagList = "", bool unmonitored = false)
{
var start = DateTime.Today.AddDays(-pastDays);
var end = DateTime.Today.AddDays(futureDays);
var tags = new List<int>();
if (tagList.IsNotNullOrWhiteSpace())
{
tags.AddRange(tagList.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
}
var movies = _movieService.GetMoviesBetweenDates(start, end, unmonitored);
var calendar = new Ical.Net.Calendar
{
ProductId = "-//radarr.video//Radarr//EN"
};
var calendarName = "Radarr Movies Calendar";
calendar.AddProperty(new CalendarProperty("NAME", calendarName));
calendar.AddProperty(new CalendarProperty("X-WR-CALNAME", calendarName));
foreach (var movie in movies.OrderBy(v => v.Added))
{
if (tags.Any() && tags.None(movie.Tags.Contains))
{
continue;
}
CreateEvent(calendar, movie.MovieMetadata, "cinematic");
CreateEvent(calendar, movie.MovieMetadata, "digital");
CreateEvent(calendar, movie.MovieMetadata, "physical");
}
var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext());
var icalendar = serializer.SerializeToString(calendar);
return Content(icalendar, "text/calendar");
}
private void CreateEvent(Ical.Net.Calendar calendar, MovieMetadata movie, string releaseType)
{
var date = movie.InCinemas;
string eventType = "_cinemas";
string summaryText = "(Theatrical Release)";
if (releaseType == "digital")
{
date = movie.DigitalRelease;
eventType = "_digital";
summaryText = "(Digital Release)";
}
else if (releaseType == "physical")
{
date = movie.PhysicalRelease;
eventType = "_physical";
summaryText = "(Physical Release)";
}
if (!date.HasValue)
{
return;
}
var occurrence = calendar.Create<CalendarEvent>();
occurrence.Uid = "Radarr_movie_" + movie.Id + eventType;
occurrence.Status = movie.Status == MovieStatusType.Announced ? EventStatus.Tentative : EventStatus.Confirmed;
occurrence.Start = new CalDateTime(date.Value);
occurrence.End = occurrence.Start;
occurrence.IsAllDay = true;
occurrence.Description = movie.Overview;
occurrence.Categories = new List<string>() { movie.Studio };
occurrence.Summary = $"{movie.Title} " + summaryText;
}
}
}

@ -0,0 +1,188 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Collections;
using NzbDrone.Core.Movies.Commands;
using NzbDrone.Core.Movies.Events;
using NzbDrone.Core.Organizer;
using NzbDrone.SignalR;
using Radarr.Http;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V4.Collections
{
[V4ApiController]
public class CollectionController : RestControllerWithSignalR<CollectionResource, MovieCollection>,
IHandle<CollectionAddedEvent>,
IHandle<CollectionEditedEvent>,
IHandle<CollectionDeletedEvent>
{
private readonly IMovieCollectionService _collectionService;
private readonly IMovieService _movieService;
private readonly IMovieMetadataService _movieMetadataService;
private readonly IBuildFileNames _fileNameBuilder;
private readonly INamingConfigService _namingService;
private readonly IManageCommandQueue _commandQueueManager;
public CollectionController(IBroadcastSignalRMessage signalRBroadcaster,
IMovieCollectionService collectionService,
IMovieService movieService,
IMovieMetadataService movieMetadataService,
IBuildFileNames fileNameBuilder,
INamingConfigService namingService,
IManageCommandQueue commandQueueManager)
: base(signalRBroadcaster)
{
_collectionService = collectionService;
_movieService = movieService;
_movieMetadataService = movieMetadataService;
_fileNameBuilder = fileNameBuilder;
_namingService = namingService;
_commandQueueManager = commandQueueManager;
}
protected override CollectionResource GetResourceById(int id)
{
return MapToResource(_collectionService.GetCollection(id));
}
[HttpGet]
public List<CollectionResource> GetCollections(int? tmdbId)
{
var collectionResources = new List<CollectionResource>();
if (tmdbId.HasValue)
{
var collection = _collectionService.FindByTmdbId(tmdbId.Value);
if (collection != null)
{
collectionResources.AddIfNotNull(MapToResource(collection));
}
}
else
{
collectionResources = MapToResource(_collectionService.GetAllCollections()).ToList();
}
return collectionResources;
}
[RestPutById]
public ActionResult<CollectionResource> UpdateCollection(CollectionResource collectionResource)
{
var collection = _collectionService.GetCollection(collectionResource.Id);
var model = collectionResource.ToModel(collection);
var updatedMovie = _collectionService.UpdateCollection(model);
return Accepted(updatedMovie.Id);
}
[HttpPut]
public ActionResult UpdateCollections(CollectionUpdateResource resource)
{
var collectionsToUpdate = _collectionService.GetCollections(resource.CollectionIds);
foreach (var collection in collectionsToUpdate)
{
if (resource.Monitored.HasValue)
{
collection.Monitored = resource.Monitored.Value;
}
if (resource.QualityProfileIds != null && resource.QualityProfileIds.Any())
{
collection.QualityProfileIds = resource.QualityProfileIds;
}
if (resource.MinimumAvailability.HasValue)
{
collection.MinimumAvailability = resource.MinimumAvailability.Value;
}
if (resource.RootFolderPath.IsNotNullOrWhiteSpace())
{
collection.RootFolderPath = resource.RootFolderPath;
}
if (resource.MonitorMovies.HasValue)
{
var movies = _movieService.GetMoviesByCollectionTmdbId(collection.TmdbId);
movies.ForEach(c => c.Monitored = resource.MonitorMovies.Value);
_movieService.UpdateMovie(movies, true);
}
}
var updated = _collectionService.UpdateCollections(collectionsToUpdate.ToList()).ToResource();
_commandQueueManager.Push(new RefreshCollectionsCommand());
return Accepted(updated);
}
private IEnumerable<CollectionResource> MapToResource(List<MovieCollection> collections)
{
// Avoid calling for naming spec on every movie in filenamebuilder
var namingConfig = _namingService.GetConfig();
var collectionMovies = _movieMetadataService.GetMoviesWithCollections();
foreach (var collection in collections)
{
var resource = collection.ToResource();
foreach (var movie in collectionMovies.Where(m => m.CollectionTmdbId == collection.TmdbId))
{
var movieResource = movie.ToResource();
movieResource.Folder = _fileNameBuilder.GetMovieFolder(new Movie { MovieMetadata = movie }, namingConfig);
resource.Movies.Add(movieResource);
}
yield return resource;
}
}
private CollectionResource MapToResource(MovieCollection collection)
{
var resource = collection.ToResource();
foreach (var movie in _movieMetadataService.GetMoviesByCollectionTmdbId(collection.TmdbId))
{
var movieResource = movie.ToResource();
movieResource.Folder = _fileNameBuilder.GetMovieFolder(new Movie { MovieMetadata = movie });
resource.Movies.Add(movieResource);
}
return resource;
}
[NonAction]
public void Handle(CollectionAddedEvent message)
{
BroadcastResourceChange(ModelAction.Created, MapToResource(message.Collection));
}
[NonAction]
public void Handle(CollectionEditedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Collection));
}
[NonAction]
public void Handle(CollectionDeletedEvent message)
{
BroadcastResourceChange(ModelAction.Deleted, message.Collection.Id);
}
}
}

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Collections;
namespace Radarr.Api.V4.Collections
{
public class CollectionMovieResource
{
public int TmdbId { get; set; }
public string ImdbId { get; set; }
public string Title { get; set; }
public string CleanTitle { get; set; }
public string SortTitle { get; set; }
public string Overview { get; set; }
public int Runtime { get; set; }
public List<MediaCover> Images { get; set; }
public int Year { get; set; }
public Ratings Ratings { get; set; }
public List<string> Genres { get; set; }
public string Folder { get; set; }
}
public static class CollectionMovieResourceMapper
{
public static CollectionMovieResource ToResource(this MovieMetadata model)
{
if (model == null)
{
return null;
}
return new CollectionMovieResource
{
TmdbId = model.TmdbId,
Title = model.Title,
Overview = model.Overview,
SortTitle = model.SortTitle,
Images = model.Images,
ImdbId = model.ImdbId,
Ratings = model.Ratings,
Runtime = model.Runtime,
CleanTitle = model.CleanTitle,
Genres = model.Genres,
Year = model.Year
};
}
}
}

@ -0,0 +1,91 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Collections;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Collections
{
public class CollectionResource : RestResource
{
public CollectionResource()
{
Movies = new List<CollectionMovieResource>();
}
public string Title { get; set; }
public string SortTitle { get; set; }
public int TmdbId { get; set; }
public List<MediaCover> Images { get; set; }
public string Overview { get; set; }
public bool Monitored { get; set; }
public string RootFolderPath { get; set; }
public List<int> QualityProfileIds { get; set; }
public bool SearchOnAdd { get; set; }
public MovieStatusType MinimumAvailability { get; set; }
public List<CollectionMovieResource> Movies { get; set; }
}
public static class CollectionResourceMapper
{
public static CollectionResource ToResource(this MovieCollection model)
{
if (model == null)
{
return null;
}
return new CollectionResource
{
Id = model.Id,
TmdbId = model.TmdbId,
Title = model.Title,
Overview = model.Overview,
SortTitle = model.SortTitle,
Monitored = model.Monitored,
Images = model.Images,
QualityProfileIds = model.QualityProfileIds,
RootFolderPath = model.RootFolderPath,
MinimumAvailability = model.MinimumAvailability,
SearchOnAdd = model.SearchOnAdd
};
}
public static List<CollectionResource> ToResource(this IEnumerable<MovieCollection> collections)
{
return collections.Select(ToResource).ToList();
}
public static MovieCollection ToModel(this CollectionResource resource)
{
if (resource == null)
{
return null;
}
return new MovieCollection
{
Id = resource.Id,
Title = resource.Title,
TmdbId = resource.TmdbId,
SortTitle = resource.SortTitle,
Overview = resource.Overview,
Monitored = resource.Monitored,
QualityProfileIds = resource.QualityProfileIds,
RootFolderPath = resource.RootFolderPath,
SearchOnAdd = resource.SearchOnAdd,
MinimumAvailability = resource.MinimumAvailability
};
}
public static MovieCollection ToModel(this CollectionResource resource, MovieCollection collection)
{
var updatedmovie = resource.ToModel();
collection.ApplyChanges(updatedmovie);
return collection;
}
}
}

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Radarr.Api.V4.Collections
{
public class CollectionUpdateCollectionResource
{
public int Id { get; set; }
public bool? Monitored { get; set; }
}
}

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Movies;
namespace Radarr.Api.V4.Collections
{
public class CollectionUpdateResource
{
public List<int> CollectionIds { get; set; }
public bool? Monitored { get; set; }
public bool? MonitorMovies { get; set; }
public List<int> QualityProfileIds { get; set; }
public string RootFolderPath { get; set; }
public MovieStatusType? MinimumAvailability { get; set; }
}
}

@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common;
using NzbDrone.Common.Composition;
using NzbDrone.Common.Serializer;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ProgressMessaging;
using NzbDrone.SignalR;
using Radarr.Http;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
using Radarr.Http.Validation;
namespace Radarr.Api.V4.Commands
{
[V4ApiController]
public class CommandController : RestControllerWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
{
private readonly IManageCommandQueue _commandQueueManager;
private readonly KnownTypes _knownTypes;
private readonly Debouncer _debouncer;
private readonly Dictionary<int, CommandResource> _pendingUpdates;
private readonly CommandPriorityComparer _commandPriorityComparer = new CommandPriorityComparer();
public CommandController(IManageCommandQueue commandQueueManager,
IBroadcastSignalRMessage signalRBroadcaster,
KnownTypes knownTypes)
: base(signalRBroadcaster)
{
_commandQueueManager = commandQueueManager;
_knownTypes = knownTypes;
_debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1));
_pendingUpdates = new Dictionary<int, CommandResource>();
PostValidator.RuleFor(c => c.Name).NotBlank();
}
protected override CommandResource GetResourceById(int id)
{
return _commandQueueManager.Get(id).ToResource();
}
[RestPostById]
public ActionResult<CommandResource> StartCommand(CommandResource commandResource)
{
var commandType =
_knownTypes.GetImplementations(typeof(Command))
.Single(c => c.Name.Replace("Command", "")
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
Request.Body.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(Request.Body))
{
var body = reader.ReadToEnd();
dynamic command = STJson.Deserialize(body, commandType);
command.Trigger = CommandTrigger.Manual;
command.SuppressMessages = !command.SendUpdatesToClient;
command.SendUpdatesToClient = true;
command.ClientUserAgent = Request.Headers["UserAgent"];
var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual);
return Created(trackedCommand.Id);
}
}
[HttpGet]
public List<CommandResource> GetStartedCommands()
{
return _commandQueueManager.All()
.OrderBy(c => c.Status, _commandPriorityComparer)
.ThenByDescending(c => c.Priority)
.ToResource();
}
[RestDeleteById]
public void CancelCommand(int id)
{
_commandQueueManager.Cancel(id);
}
[NonAction]
public void Handle(CommandUpdatedEvent message)
{
if (message.Command.Body.SendUpdatesToClient)
{
lock (_pendingUpdates)
{
_pendingUpdates[message.Command.Id] = message.Command.ToResource();
}
_debouncer.Execute();
}
}
private void SendUpdates()
{
lock (_pendingUpdates)
{
var pendingUpdates = _pendingUpdates.Values.ToArray();
_pendingUpdates.Clear();
foreach (var pendingUpdate in pendingUpdates)
{
BroadcastResourceChange(ModelAction.Updated, pendingUpdate);
if (pendingUpdate.Name == typeof(MessagingCleanupCommand).Name.Replace("Command", "") &&
pendingUpdate.Status == CommandStatus.Completed)
{
BroadcastResourceChange(ModelAction.Sync);
}
}
}
}
}
}

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Messaging.Commands;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Commands
{
public class CommandResource : RestResource
{
public string Name { get; set; }
public string CommandName { get; set; }
public string Message { get; set; }
public Command Body { get; set; }
public CommandPriority Priority { get; set; }
public CommandStatus Status { get; set; }
public DateTime Queued { get; set; }
public DateTime? Started { get; set; }
public DateTime? Ended { get; set; }
public TimeSpan? Duration { get; set; }
public string Exception { get; set; }
public CommandTrigger Trigger { get; set; }
public string ClientUserAgent { get; set; }
[JsonIgnore]
public string CompletionMessage { get; set; }
public DateTime? StateChangeTime
{
get
{
if (Started.HasValue)
{
return Started.Value;
}
return Ended;
}
set
{
}
}
public bool SendUpdatesToClient
{
get
{
if (Body != null)
{
return Body.SendUpdatesToClient;
}
return false;
}
set
{
}
}
public bool UpdateScheduledTask
{
get
{
if (Body != null)
{
return Body.UpdateScheduledTask;
}
return false;
}
set
{
}
}
public DateTime? LastExecutionTime { get; set; }
}
public static class CommandResourceMapper
{
public static CommandResource ToResource(this CommandModel model)
{
if (model == null)
{
return null;
}
return new CommandResource
{
Id = model.Id,
Name = model.Name,
CommandName = model.Name.SplitCamelCase(),
Message = model.Message,
Body = model.Body,
Priority = model.Priority,
Status = model.Status,
Queued = model.QueuedAt,
Started = model.StartedAt,
Ended = model.EndedAt,
Duration = model.Duration,
Exception = model.Exception,
Trigger = model.Trigger,
ClientUserAgent = UserAgentParser.SimplifyUserAgent(model.Body.ClientUserAgent),
CompletionMessage = model.Body.CompletionMessage,
LastExecutionTime = model.Body.LastExecutionTime
};
}
public static List<CommandResource> ToResource(this IEnumerable<CommandModel> models)
{
return models.Select(ToResource).ToList();
}
}
}

@ -0,0 +1,48 @@
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Configuration;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V4.Config
{
public abstract class ConfigController<TResource> : RestController<TResource>
where TResource : RestResource, new()
{
protected readonly IConfigService _configService;
protected ConfigController(IConfigService configService)
{
_configService = configService;
}
protected override TResource GetResourceById(int id)
{
return GetConfig();
}
[HttpGet]
public TResource GetConfig()
{
var resource = ToResource(_configService);
resource.Id = 1;
return resource;
}
[RestPutById]
public virtual ActionResult<TResource> SaveConfig(TResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
_configService.SaveConfigDictionary(dictionary);
return Accepted(resource.Id);
}
protected abstract TResource ToResource(IConfigService model);
}
}

@ -0,0 +1,19 @@
using NzbDrone.Core.Configuration;
using Radarr.Http;
namespace Radarr.Api.V4.Config
{
[V4ApiController("config/downloadclient")]
public class DownloadClientConfigController : ConfigController<DownloadClientConfigResource>
{
public DownloadClientConfigController(IConfigService configService)
: base(configService)
{
}
protected override DownloadClientConfigResource ToResource(IConfigService model)
{
return DownloadClientConfigResourceMapper.ToResource(model);
}
}
}

@ -0,0 +1,31 @@
using NzbDrone.Core.Configuration;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Config
{
public class DownloadClientConfigResource : RestResource
{
public string DownloadClientWorkingFolders { get; set; }
public bool EnableCompletedDownloadHandling { get; set; }
public int CheckForFinishedDownloadInterval { get; set; }
public bool AutoRedownloadFailed { get; set; }
}
public static class DownloadClientConfigResourceMapper
{
public static DownloadClientConfigResource ToResource(IConfigService model)
{
return new DownloadClientConfigResource
{
DownloadClientWorkingFolders = model.DownloadClientWorkingFolders,
EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling,
CheckForFinishedDownloadInterval = model.CheckForFinishedDownloadInterval,
AutoRedownloadFailed = model.AutoRedownloadFailed
};
}
}
}

@ -0,0 +1,121 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Update;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using Radarr.Http;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V4.Config
{
[V4ApiController("config/host")]
public class HostConfigController : RestController<HostConfigResource>
{
private readonly IConfigFileProvider _configFileProvider;
private readonly IConfigService _configService;
private readonly IUserService _userService;
public HostConfigController(IConfigFileProvider configFileProvider,
IConfigService configService,
IUserService userService,
FileExistsValidator fileExistsValidator)
{
_configFileProvider = configFileProvider;
_configService = configService;
_userService = userService;
SharedValidator.RuleFor(c => c.BindAddress)
.ValidIp4Address()
.NotListenAllIp4Address()
.When(c => c.BindAddress != "*");
SharedValidator.RuleFor(c => c.Port).ValidPort();
SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase();
SharedValidator.RuleFor(c => c.InstanceName).ContainsRadarr().When(c => c.InstanceName.IsNotNullOrWhiteSpace());
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl);
SharedValidator.RuleFor(c => c.SslPort).NotEqual(c => c.Port).When(c => c.EnableSsl);
SharedValidator.RuleFor(c => c.SslCertPath)
.Cascade(CascadeMode.StopOnFirstFailure)
.NotEmpty()
.IsValidPath()
.SetValidator(fileExistsValidator)
.Must((resource, path) => IsValidSslCertificate(resource)).WithMessage("Invalid SSL certificate file or password")
.When(c => c.EnableSsl);
SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default");
SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script);
SharedValidator.RuleFor(c => c.BackupFolder).IsValidPath().When(c => Path.IsPathRooted(c.BackupFolder));
SharedValidator.RuleFor(c => c.BackupInterval).InclusiveBetween(1, 7);
SharedValidator.RuleFor(c => c.BackupRetention).InclusiveBetween(1, 90);
}
private bool IsValidSslCertificate(HostConfigResource resource)
{
X509Certificate2 cert;
try
{
cert = new X509Certificate2(resource.SslCertPath, resource.SslCertPassword, X509KeyStorageFlags.DefaultKeySet);
}
catch
{
return false;
}
return cert != null;
}
protected override HostConfigResource GetResourceById(int id)
{
return GetHostConfig();
}
[HttpGet]
public HostConfigResource GetHostConfig()
{
var resource = _configFileProvider.ToResource(_configService);
resource.Id = 1;
var user = _userService.FindUser();
if (user != null)
{
resource.Username = user.Username;
resource.Password = user.Password;
}
return resource;
}
[RestPutById]
public ActionResult<HostConfigResource> SaveHostConfig(HostConfigResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
_configFileProvider.SaveConfigDictionary(dictionary);
_configService.SaveConfigDictionary(dictionary);
if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace())
{
_userService.Upsert(resource.Username, resource.Password);
}
return Accepted(resource.Id);
}
}
}

@ -0,0 +1,93 @@
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Core.Update;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Config
{
public class HostConfigResource : RestResource
{
public string BindAddress { get; set; }
public int Port { get; set; }
public int SslPort { get; set; }
public bool EnableSsl { get; set; }
public bool LaunchBrowser { get; set; }
public AuthenticationType AuthenticationMethod { get; set; }
public AuthenticationRequiredType AuthenticationRequired { get; set; }
public bool AnalyticsEnabled { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string LogLevel { get; set; }
public string ConsoleLogLevel { get; set; }
public string Branch { get; set; }
public string ApiKey { get; set; }
public string SslCertPath { get; set; }
public string SslCertPassword { get; set; }
public string UrlBase { get; set; }
public string InstanceName { get; set; }
public string ApplicationUrl { get; set; }
public bool UpdateAutomatically { get; set; }
public UpdateMechanism UpdateMechanism { get; set; }
public string UpdateScriptPath { get; set; }
public bool ProxyEnabled { get; set; }
public ProxyType ProxyType { get; set; }
public string ProxyHostname { get; set; }
public int ProxyPort { get; set; }
public string ProxyUsername { get; set; }
public string ProxyPassword { get; set; }
public string ProxyBypassFilter { get; set; }
public bool ProxyBypassLocalAddresses { get; set; }
public CertificateValidationType CertificateValidation { get; set; }
public string BackupFolder { get; set; }
public int BackupInterval { get; set; }
public int BackupRetention { get; set; }
}
public static class HostConfigResourceMapper
{
public static HostConfigResource ToResource(this IConfigFileProvider model, IConfigService configService)
{
// TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead?
return new HostConfigResource
{
BindAddress = model.BindAddress,
Port = model.Port,
SslPort = model.SslPort,
EnableSsl = model.EnableSsl,
LaunchBrowser = model.LaunchBrowser,
AuthenticationMethod = model.AuthenticationMethod,
AuthenticationRequired = model.AuthenticationRequired,
AnalyticsEnabled = model.AnalyticsEnabled,
// Username
// Password
LogLevel = model.LogLevel,
ConsoleLogLevel = model.ConsoleLogLevel,
Branch = model.Branch,
ApiKey = model.ApiKey,
SslCertPath = model.SslCertPath,
SslCertPassword = model.SslCertPassword,
UrlBase = model.UrlBase,
InstanceName = model.InstanceName,
UpdateAutomatically = model.UpdateAutomatically,
UpdateMechanism = model.UpdateMechanism,
UpdateScriptPath = model.UpdateScriptPath,
ProxyEnabled = configService.ProxyEnabled,
ProxyType = configService.ProxyType,
ProxyHostname = configService.ProxyHostname,
ProxyPort = configService.ProxyPort,
ProxyUsername = configService.ProxyUsername,
ProxyPassword = configService.ProxyPassword,
ProxyBypassFilter = configService.ProxyBypassFilter,
ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses,
CertificateValidation = configService.CertificateValidation,
BackupFolder = configService.BackupFolder,
BackupInterval = configService.BackupInterval,
BackupRetention = configService.BackupRetention,
ApplicationUrl = configService.ApplicationUrl
};
}
}
}

@ -0,0 +1,23 @@
using NzbDrone.Core.Configuration;
using Radarr.Http;
using Radarr.Http.Validation;
namespace Radarr.Api.V4.Config
{
[V4ApiController("config/importlist")]
public class ImportListConfigController : ConfigController<ImportListConfigResource>
{
public ImportListConfigController(IConfigService configService)
: base(configService)
{
SharedValidator.RuleFor(c => c.ImportListSyncInterval)
.IsValidImportListSyncInterval();
}
protected override ImportListConfigResource ToResource(IConfigService model)
{
return ImportListConfigResourceMapper.ToResource(model);
}
}
}

@ -0,0 +1,25 @@
using NzbDrone.Core.Configuration;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Config
{
public class ImportListConfigResource : RestResource
{
public int ImportListSyncInterval { get; set; }
public string ListSyncLevel { get; set; }
public string ImportExclusions { get; set; }
}
public static class ImportListConfigResourceMapper
{
public static ImportListConfigResource ToResource(IConfigService model)
{
return new ImportListConfigResource
{
ImportListSyncInterval = model.ImportListSyncInterval,
ListSyncLevel = model.ListSyncLevel,
ImportExclusions = model.ImportExclusions
};
}
}
}

@ -0,0 +1,32 @@
using FluentValidation;
using NzbDrone.Core.Configuration;
using Radarr.Http;
using Radarr.Http.Validation;
namespace Radarr.Api.V4.Config
{
[V4ApiController("config/indexer")]
public class IndexerConfigController : ConfigController<IndexerConfigResource>
{
public IndexerConfigController(IConfigService configService)
: base(configService)
{
SharedValidator.RuleFor(c => c.MinimumAge)
.GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.MaximumSize)
.GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.Retention)
.GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.RssSyncInterval)
.IsValidRssSyncInterval();
}
protected override IndexerConfigResource ToResource(IConfigService model)
{
return IndexerConfigResourceMapper.ToResource(model);
}
}
}

@ -0,0 +1,35 @@
using NzbDrone.Core.Configuration;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Config
{
public class IndexerConfigResource : RestResource
{
public int MinimumAge { get; set; }
public int MaximumSize { get; set; }
public int Retention { get; set; }
public int RssSyncInterval { get; set; }
public bool PreferIndexerFlags { get; set; }
public int AvailabilityDelay { get; set; }
public bool AllowHardcodedSubs { get; set; }
public string WhitelistedHardcodedSubs { get; set; }
}
public static class IndexerConfigResourceMapper
{
public static IndexerConfigResource ToResource(IConfigService model)
{
return new IndexerConfigResource
{
MinimumAge = model.MinimumAge,
MaximumSize = model.MaximumSize,
Retention = model.Retention,
RssSyncInterval = model.RssSyncInterval,
PreferIndexerFlags = model.PreferIndexerFlags,
AvailabilityDelay = model.AvailabilityDelay,
AllowHardcodedSubs = model.AllowHardcodedSubs,
WhitelistedHardcodedSubs = model.WhitelistedHardcodedSubs,
};
}
}
}

@ -0,0 +1,45 @@
using FluentValidation;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using Radarr.Http;
namespace Radarr.Api.V4.Config
{
[V4ApiController("config/mediamanagement")]
public class MediaManagementConfigController : ConfigController<MediaManagementConfigResource>
{
public MediaManagementConfigController(IConfigService configService,
PathExistsValidator pathExistsValidator,
FolderChmodValidator folderChmodValidator,
FolderWritableValidator folderWritableValidator,
MoviePathValidator moviePathValidator,
StartupFolderValidator startupFolderValidator,
SystemFolderValidator systemFolderValidator,
RootFolderAncestorValidator rootFolderAncestorValidator,
RootFolderValidator rootFolderValidator)
: base(configService)
{
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && (OsInfo.IsLinux || OsInfo.IsOsx));
SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath()
.SetValidator(folderWritableValidator)
.SetValidator(rootFolderValidator)
.SetValidator(pathExistsValidator)
.SetValidator(rootFolderAncestorValidator)
.SetValidator(startupFolderValidator)
.SetValidator(systemFolderValidator)
.SetValidator(moviePathValidator)
.When(c => !string.IsNullOrWhiteSpace(c.RecycleBin));
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
}
protected override MediaManagementConfigResource ToResource(IConfigService model)
{
return MediaManagementConfigResourceMapper.ToResource(model);
}
}
}

@ -0,0 +1,62 @@
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Config
{
public class MediaManagementConfigResource : RestResource
{
public bool AutoUnmonitorPreviouslyDownloadedMovies { get; set; }
public string RecycleBin { get; set; }
public int RecycleBinCleanupDays { get; set; }
public ProperDownloadTypes DownloadPropersAndRepacks { get; set; }
public bool CreateEmptyMovieFolders { get; set; }
public bool DeleteEmptyFolders { get; set; }
public FileDateType FileDate { get; set; }
public RescanAfterRefreshType RescanAfterRefresh { get; set; }
public bool AutoRenameFolders { get; set; }
public bool PathsDefaultStatic { get; set; }
public bool SetPermissionsLinux { get; set; }
public string ChmodFolder { get; set; }
public string ChownGroup { get; set; }
public bool SkipFreeSpaceCheckWhenImporting { get; set; }
public int MinimumFreeSpaceWhenImporting { get; set; }
public bool CopyUsingHardlinks { get; set; }
public bool ImportExtraFiles { get; set; }
public string ExtraFileExtensions { get; set; }
public bool EnableMediaInfo { get; set; }
}
public static class MediaManagementConfigResourceMapper
{
public static MediaManagementConfigResource ToResource(IConfigService model)
{
return new MediaManagementConfigResource
{
AutoUnmonitorPreviouslyDownloadedMovies = model.AutoUnmonitorPreviouslyDownloadedMovies,
RecycleBin = model.RecycleBin,
RecycleBinCleanupDays = model.RecycleBinCleanupDays,
DownloadPropersAndRepacks = model.DownloadPropersAndRepacks,
CreateEmptyMovieFolders = model.CreateEmptyMovieFolders,
DeleteEmptyFolders = model.DeleteEmptyFolders,
FileDate = model.FileDate,
RescanAfterRefresh = model.RescanAfterRefresh,
AutoRenameFolders = model.AutoRenameFolders,
SetPermissionsLinux = model.SetPermissionsLinux,
ChmodFolder = model.ChmodFolder,
ChownGroup = model.ChownGroup,
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
MinimumFreeSpaceWhenImporting = model.MinimumFreeSpaceWhenImporting,
CopyUsingHardlinks = model.CopyUsingHardlinks,
ImportExtraFiles = model.ImportExtraFiles,
ExtraFileExtensions = model.ExtraFileExtensions,
EnableMediaInfo = model.EnableMediaInfo
};
}
}
}

@ -0,0 +1,19 @@
using NzbDrone.Core.Configuration;
using Radarr.Http;
namespace Radarr.Api.V4.Config
{
[V4ApiController("config/metadata")]
public class MetadataConfigController : ConfigController<MetadataConfigResource>
{
public MetadataConfigController(IConfigService configService)
: base(configService)
{
}
protected override MetadataConfigResource ToResource(IConfigService model)
{
return MetadataConfigResourceMapper.ToResource(model);
}
}
}

@ -0,0 +1,22 @@
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Config
{
public class MetadataConfigResource : RestResource
{
public TMDbCountryCode CertificationCountry { get; set; }
}
public static class MetadataConfigResourceMapper
{
public static MetadataConfigResource ToResource(IConfigService model)
{
return new MetadataConfigResource
{
CertificationCountry = model.CertificationCountry,
};
}
}
}

@ -0,0 +1,105 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Organizer;
using Radarr.Http;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V4.Config
{
[V4ApiController("config/naming")]
public class NamingConfigController : RestController<NamingConfigResource>
{
private readonly INamingConfigService _namingConfigService;
private readonly IFilenameSampleService _filenameSampleService;
private readonly IFilenameValidationService _filenameValidationService;
private readonly IBuildFileNames _filenameBuilder;
public NamingConfigController(INamingConfigService namingConfigService,
IFilenameSampleService filenameSampleService,
IFilenameValidationService filenameValidationService,
IBuildFileNames filenameBuilder)
{
_namingConfigService = namingConfigService;
_filenameSampleService = filenameSampleService;
_filenameValidationService = filenameValidationService;
_filenameBuilder = filenameBuilder;
SharedValidator.RuleFor(c => c.StandardMovieFormat).ValidMovieFormat();
SharedValidator.RuleFor(c => c.MovieFolderFormat).ValidMovieFolderFormat();
}
protected override NamingConfigResource GetResourceById(int id)
{
return GetNamingConfig();
}
[HttpGet]
public NamingConfigResource GetNamingConfig()
{
var nameSpec = _namingConfigService.GetConfig();
var resource = nameSpec.ToResource();
if (resource.StandardMovieFormat.IsNotNullOrWhiteSpace())
{
var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec);
basicConfig.AddToResource(resource);
}
return resource;
}
[RestPutById]
public ActionResult<NamingConfigResource> UpdateNamingConfig(NamingConfigResource resource)
{
var nameSpec = resource.ToModel();
ValidateFormatResult(nameSpec);
_namingConfigService.Save(nameSpec);
return Accepted(resource.Id);
}
[HttpGet("examples")]
public object GetExamples([FromQuery]NamingConfigResource config)
{
if (config.Id == 0)
{
config = GetNamingConfig();
}
var nameSpec = config.ToModel();
var sampleResource = new NamingExampleResource();
var movieSampleResult = _filenameSampleService.GetMovieSample(nameSpec);
sampleResource.MovieExample = nameSpec.StandardMovieFormat.IsNullOrWhiteSpace()
? "Invalid Format"
: movieSampleResult.FileName;
sampleResource.MovieFolderExample = nameSpec.MovieFolderFormat.IsNullOrWhiteSpace()
? "Invalid format"
: _filenameSampleService.GetMovieFolderSample(nameSpec);
return sampleResource;
}
private void ValidateFormatResult(NamingConfig nameSpec)
{
var movieSampleResult = _filenameSampleService.GetMovieSample(nameSpec);
var standardMovieValidationResult = _filenameValidationService.ValidateMovieFilename(movieSampleResult);
var validationFailures = new List<ValidationFailure>();
if (validationFailures.Any())
{
throw new ValidationException(validationFailures.DistinctBy(v => v.PropertyName).ToArray());
}
}
}
}

@ -0,0 +1,18 @@
using NzbDrone.Core.Organizer;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Config
{
public class NamingConfigResource : RestResource
{
public bool RenameMovies { get; set; }
public bool ReplaceIllegalCharacters { get; set; }
public ColonReplacementFormat ColonReplacementFormat { get; set; }
public string StandardMovieFormat { get; set; }
public string MovieFolderFormat { get; set; }
public bool IncludeQuality { get; set; }
public bool ReplaceSpaces { get; set; }
public string Separator { get; set; }
public string NumberStyle { get; set; }
}
}

@ -0,0 +1,54 @@
using NzbDrone.Core.Organizer;
namespace Radarr.Api.V4.Config
{
public class NamingExampleResource
{
public string MovieExample { get; set; }
public string MovieFolderExample { get; set; }
}
public static class NamingConfigResourceMapper
{
public static NamingConfigResource ToResource(this NamingConfig model)
{
return new NamingConfigResource
{
Id = model.Id,
RenameMovies = model.RenameMovies,
ReplaceIllegalCharacters = model.ReplaceIllegalCharacters,
ColonReplacementFormat = model.ColonReplacementFormat,
StandardMovieFormat = model.StandardMovieFormat,
MovieFolderFormat = model.MovieFolderFormat,
// IncludeQuality
// ReplaceSpaces
// Separator
// NumberStyle
};
}
public static void AddToResource(this BasicNamingConfig basicNamingConfig, NamingConfigResource resource)
{
resource.IncludeQuality = basicNamingConfig.IncludeQuality;
resource.ReplaceSpaces = basicNamingConfig.ReplaceSpaces;
resource.Separator = basicNamingConfig.Separator;
resource.NumberStyle = basicNamingConfig.NumberStyle;
}
public static NamingConfig ToModel(this NamingConfigResource resource)
{
return new NamingConfig
{
Id = resource.Id,
RenameMovies = resource.RenameMovies,
ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters,
ColonReplacementFormat = resource.ColonReplacementFormat,
StandardMovieFormat = resource.StandardMovieFormat,
MovieFolderFormat = resource.MovieFolderFormat,
};
}
}
}

@ -0,0 +1,39 @@
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Configuration;
using Radarr.Http;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V4.Config
{
[V4ApiController("config/ui")]
public class UiConfigController : ConfigController<UiConfigResource>
{
private readonly IConfigFileProvider _configFileProvider;
public UiConfigController(IConfigFileProvider configFileProvider, IConfigService configService)
: base(configService)
{
_configFileProvider = configFileProvider;
}
[RestPutById]
public override ActionResult<UiConfigResource> SaveConfig(UiConfigResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
_configFileProvider.SaveConfigDictionary(dictionary);
_configService.SaveConfigDictionary(dictionary);
return Accepted(resource.Id);
}
protected override UiConfigResource ToResource(IConfigService model)
{
return UiConfigResourceMapper.ToResource(_configFileProvider, model);
}
}
}

@ -0,0 +1,50 @@
using NzbDrone.Core.Configuration;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Config
{
public class UiConfigResource : RestResource
{
// Calendar
public int FirstDayOfWeek { get; set; }
public string CalendarWeekColumnHeader { get; set; }
// Movies
public MovieRuntimeFormatType MovieRuntimeFormat { get; set; }
// Dates
public string ShortDateFormat { get; set; }
public string LongDateFormat { get; set; }
public string TimeFormat { get; set; }
public bool ShowRelativeDates { get; set; }
public bool EnableColorImpairedMode { get; set; }
public int MovieInfoLanguage { get; set; }
public int UILanguage { get; set; }
public string Theme { get; set; }
}
public static class UiConfigResourceMapper
{
public static UiConfigResource ToResource(IConfigFileProvider config, IConfigService model)
{
return new UiConfigResource
{
FirstDayOfWeek = model.FirstDayOfWeek,
CalendarWeekColumnHeader = model.CalendarWeekColumnHeader,
MovieRuntimeFormat = model.MovieRuntimeFormat,
ShortDateFormat = model.ShortDateFormat,
LongDateFormat = model.LongDateFormat,
TimeFormat = model.TimeFormat,
ShowRelativeDates = model.ShowRelativeDates,
EnableColorImpairedMode = model.EnableColorImpairedMode,
MovieInfoLanguage = model.MovieInfoLanguage,
UILanguage = model.UILanguage,
Theme = config.Theme
};
}
}
}

@ -0,0 +1,44 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Credits;
using Radarr.Http;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Credits
{
[V4ApiController]
public class CreditController : RestController<CreditResource>
{
private readonly ICreditService _creditService;
private readonly IMovieService _movieService;
public CreditController(ICreditService creditService, IMovieService movieService)
{
_creditService = creditService;
_movieService = movieService;
}
protected override CreditResource GetResourceById(int id)
{
return _creditService.GetById(id).ToResource();
}
[HttpGet]
public List<CreditResource> GetCredits(int? movieId, int? movieMetadataId)
{
if (movieMetadataId.HasValue)
{
return _creditService.GetAllCreditsForMovieMetadata(movieMetadataId.Value).ToResource();
}
if (movieId.HasValue)
{
var movie = _movieService.GetMovie(movieId.Value);
return _creditService.GetAllCreditsForMovieMetadata(movie.MovieMetadataId).ToResource();
}
return _creditService.GetAllCredits().ToResource();
}
}
}

@ -0,0 +1,80 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Movies.Credits;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Credits
{
public class CreditResource : RestResource
{
public CreditResource()
{
}
public string PersonName { get; set; }
public string CreditTmdbId { get; set; }
public int PersonTmdbId { get; set; }
public int MovieMetadataId { get; set; }
public List<MediaCover> Images { get; set; }
public string Department { get; set; }
public string Job { get; set; }
public string Character { get; set; }
public int Order { get; set; }
public CreditType Type { get; set; }
}
public static class CreditResourceMapper
{
public static CreditResource ToResource(this Credit model)
{
if (model == null)
{
return null;
}
return new CreditResource
{
Id = model.Id,
MovieMetadataId = model.MovieMetadataId,
CreditTmdbId = model.CreditTmdbId,
PersonTmdbId = model.PersonTmdbId,
PersonName = model.Name,
Order = model.Order,
Character = model.Character,
Department = model.Department,
Images = model.Images,
Job = model.Job,
Type = model.Type
};
}
public static List<CreditResource> ToResource(this IEnumerable<Credit> credits)
{
return credits.Select(ToResource).ToList();
}
public static Credit ToModel(this CreditResource resource)
{
if (resource == null)
{
return null;
}
return new Credit
{
Id = resource.Id,
MovieMetadataId = resource.MovieMetadataId,
Name = resource.PersonName,
Order = resource.Order,
Character = resource.Character,
Department = resource.Department,
Job = resource.Job,
Type = resource.Type,
Images = resource.Images,
CreditTmdbId = resource.CreditTmdbId,
PersonTmdbId = resource.PersonTmdbId
};
}
}
}

@ -0,0 +1,52 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.CustomFilters;
using Radarr.Http;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V4.CustomFilters
{
[V4ApiController]
public class CustomFilterController : RestController<CustomFilterResource>
{
private readonly ICustomFilterService _customFilterService;
public CustomFilterController(ICustomFilterService customFilterService)
{
_customFilterService = customFilterService;
}
protected override CustomFilterResource GetResourceById(int id)
{
return _customFilterService.Get(id).ToResource();
}
[HttpGet]
public List<CustomFilterResource> GetCustomFilters()
{
return _customFilterService.All().ToResource();
}
[RestPostById]
public ActionResult<CustomFilterResource> AddCustomFilter(CustomFilterResource resource)
{
var customFilter = _customFilterService.Add(resource.ToModel());
return Created(customFilter.Id);
}
[RestPutById]
public ActionResult<CustomFilterResource> UpdateCustomFilter(CustomFilterResource resource)
{
_customFilterService.Update(resource.ToModel());
return Accepted(resource.Id);
}
[RestDeleteById]
public void DeleteCustomResource(int id)
{
_customFilterService.Delete(id);
}
}
}

@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.CustomFilters;
using Radarr.Http.REST;
namespace Radarr.Api.V4.CustomFilters
{
public class CustomFilterResource : RestResource
{
public string Type { get; set; }
public string Label { get; set; }
public List<ExpandoObject> Filters { get; set; }
}
public static class CustomFilterResourceMapper
{
public static CustomFilterResource ToResource(this CustomFilter model)
{
if (model == null)
{
return null;
}
return new CustomFilterResource
{
Id = model.Id,
Type = model.Type,
Label = model.Label,
Filters = STJson.Deserialize<List<ExpandoObject>>(model.Filters)
};
}
public static CustomFilter ToModel(this CustomFilterResource resource)
{
if (resource == null)
{
return null;
}
return new CustomFilter
{
Id = resource.Id,
Type = resource.Type,
Label = resource.Label,
Filters = STJson.ToJson(resource.Filters)
};
}
public static List<CustomFilterResource> ToResource(this IEnumerable<CustomFilter> filters)
{
return filters.Select(ToResource).ToList();
}
}
}

@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Validation;
using Radarr.Http;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V4.CustomFormats
{
[V4ApiController]
public class CustomFormatController : RestController<CustomFormatResource>
{
private readonly ICustomFormatService _formatService;
private readonly List<ICustomFormatSpecification> _specifications;
public CustomFormatController(ICustomFormatService formatService,
List<ICustomFormatSpecification> specifications)
{
_formatService = formatService;
_specifications = specifications;
SharedValidator.RuleFor(c => c.Name).NotEmpty();
SharedValidator.RuleFor(c => c.Name)
.Must((v, c) => !_formatService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique.");
SharedValidator.RuleFor(c => c).Custom((customFormat, context) =>
{
if (!customFormat.Specifications.Any())
{
context.AddFailure("Must contain at least one Condition");
}
if (customFormat.Specifications.Any(s => s.Name.IsNullOrWhiteSpace()))
{
context.AddFailure("Condition name(s) cannot be empty or consist of only spaces");
}
});
}
protected override CustomFormatResource GetResourceById(int id)
{
return _formatService.GetById(id).ToResource();
}
[RestPostById]
public ActionResult<CustomFormatResource> Create(CustomFormatResource customFormatResource)
{
var model = customFormatResource.ToModel(_specifications);
Validate(model);
return Created(_formatService.Insert(model).Id);
}
[RestPutById]
public ActionResult<CustomFormatResource> Update(CustomFormatResource resource)
{
var model = resource.ToModel(_specifications);
Validate(model);
_formatService.Update(model);
return Accepted(model.Id);
}
[HttpGet]
public List<CustomFormatResource> GetAll()
{
return _formatService.All().ToResource();
}
[RestDeleteById]
public void DeleteFormat(int id)
{
_formatService.Delete(id);
}
[HttpGet("schema")]
public object GetTemplates()
{
var schema = _specifications.OrderBy(x => x.Order).Select(x => x.ToSchema()).ToList();
var presets = GetPresets();
foreach (var item in schema)
{
item.Presets = presets.Where(x => x.GetType().Name == item.Implementation).Select(x => x.ToSchema()).ToList();
}
return schema;
}
private void Validate(CustomFormat definition)
{
foreach (var spec in definition.Specifications)
{
var validationResult = spec.Validate();
VerifyValidationResult(validationResult);
}
}
protected void VerifyValidationResult(ValidationResult validationResult)
{
var result = new NzbDroneValidationResult(validationResult.Errors);
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
}
private IEnumerable<ICustomFormatSpecification> GetPresets()
{
yield return new ReleaseTitleSpecification
{
Name = "x264",
Value = @"(x|h)\.?264"
};
yield return new ReleaseTitleSpecification
{
Name = "x265",
Value = @"(((x|h)\.?265)|(HEVC))"
};
yield return new ReleaseTitleSpecification
{
Name = "Simple Hardcoded Subs",
Value = @"subs?"
};
yield return new ReleaseTitleSpecification
{
Name = "Hardcoded Subs",
Value = @"\b(?<hcsub>(\w+SUBS?)\b)|(?<hc>(HC|SUBBED))\b"
};
yield return new ReleaseTitleSpecification
{
Name = "Surround Sound",
Value = @"DTS.?(HD|ES|X(?!\D))|TRUEHD|ATMOS|DD(\+|P).?([5-9])|EAC3.?([5-9])"
};
yield return new ReleaseTitleSpecification
{
Name = "Preferred Words",
Value = @"\b(SPARKS|Framestor)\b"
};
var formats = _formatService.All();
foreach (var format in formats)
{
foreach (var condition in format.Specifications)
{
var preset = condition.Clone();
preset.Name = $"{format.Name}: {preset.Name}";
yield return preset;
}
}
}
}
}

@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using NzbDrone.Core.CustomFormats;
using Radarr.Http.ClientSchema;
using Radarr.Http.REST;
namespace Radarr.Api.V4.CustomFormats
{
public class CustomFormatResource : RestResource
{
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override int Id { get; set; }
public string Name { get; set; }
public bool IncludeCustomFormatWhenRenaming { get; set; }
public List<CustomFormatSpecificationSchema> Specifications { get; set; }
}
public static class CustomFormatResourceMapper
{
public static CustomFormatResource ToResource(this CustomFormat model)
{
return new CustomFormatResource
{
Id = model.Id,
Name = model.Name,
IncludeCustomFormatWhenRenaming = model.IncludeCustomFormatWhenRenaming,
Specifications = model.Specifications.Select(x => x.ToSchema()).ToList()
};
}
public static List<CustomFormatResource> ToResource(this IEnumerable<CustomFormat> models)
{
return models.Select(m => m.ToResource()).ToList();
}
public static CustomFormat ToModel(this CustomFormatResource resource, List<ICustomFormatSpecification> specifications)
{
return new CustomFormat
{
Id = resource.Id,
Name = resource.Name,
IncludeCustomFormatWhenRenaming = resource.IncludeCustomFormatWhenRenaming,
Specifications = resource.Specifications.Select(x => MapSpecification(x, specifications)).ToList()
};
}
private static ICustomFormatSpecification MapSpecification(CustomFormatSpecificationSchema resource, List<ICustomFormatSpecification> specifications)
{
var type = specifications.SingleOrDefault(x => x.GetType().Name == resource.Implementation).GetType();
var spec = (ICustomFormatSpecification)SchemaBuilder.ReadFromSchema(resource.Fields, type);
spec.Name = resource.Name;
spec.Negate = resource.Negate;
spec.Required = resource.Required;
return spec;
}
}
}

@ -0,0 +1,36 @@
using System.Collections.Generic;
using NzbDrone.Core.CustomFormats;
using Radarr.Http.ClientSchema;
using Radarr.Http.REST;
namespace Radarr.Api.V4.CustomFormats
{
public class CustomFormatSpecificationSchema : RestResource
{
public string Name { get; set; }
public string Implementation { get; set; }
public string ImplementationName { get; set; }
public string InfoLink { get; set; }
public bool Negate { get; set; }
public bool Required { get; set; }
public List<Field> Fields { get; set; }
public List<CustomFormatSpecificationSchema> Presets { get; set; }
}
public static class CustomFormatSpecificationSchemaMapper
{
public static CustomFormatSpecificationSchema ToSchema(this ICustomFormatSpecification model)
{
return new CustomFormatSpecificationSchema
{
Name = model.Name,
Implementation = model.GetType().Name,
ImplementationName = model.ImplementationName,
InfoLink = model.InfoLink,
Negate = model.Negate,
Required = model.Required,
Fields = SchemaBuilder.ToSchema(model)
};
}
}
}

@ -0,0 +1,24 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.DiskSpace;
using Radarr.Http;
namespace Radarr.Api.V4.DiskSpace
{
[V4ApiController("diskspace")]
public class DiskSpaceController : Controller
{
private readonly IDiskSpaceService _diskSpaceService;
public DiskSpaceController(IDiskSpaceService diskSpaceService)
{
_diskSpaceService = diskSpaceService;
}
[HttpGet]
public List<DiskSpaceResource> GetFreeSpace()
{
return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource);
}
}
}

@ -0,0 +1,31 @@
using Radarr.Http.REST;
namespace Radarr.Api.V4.DiskSpace
{
public class DiskSpaceResource : RestResource
{
public string Path { get; set; }
public string Label { get; set; }
public long FreeSpace { get; set; }
public long TotalSpace { get; set; }
}
public static class DiskSpaceResourceMapper
{
public static DiskSpaceResource MapToResource(this NzbDrone.Core.DiskSpace.DiskSpace model)
{
if (model == null)
{
return null;
}
return new DiskSpaceResource
{
Path = model.Path,
Label = model.Label,
FreeSpace = model.FreeSpace,
TotalSpace = model.TotalSpace
};
}
}
}

@ -0,0 +1,16 @@
using NzbDrone.Core.Download;
using Radarr.Http;
namespace Radarr.Api.V4.DownloadClient
{
[V4ApiController]
public class DownloadClientController : ProviderControllerBase<DownloadClientResource, IDownloadClient, DownloadClientDefinition>
{
public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper();
public DownloadClientController(IDownloadClientFactory downloadClientFactory)
: base(downloadClientFactory, "downloadclient", ResourceMapper)
{
}
}
}

@ -0,0 +1,53 @@
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
namespace Radarr.Api.V4.DownloadClient
{
public class DownloadClientResource : ProviderResource<DownloadClientResource>
{
public bool Enable { get; set; }
public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; }
public bool RemoveCompletedDownloads { get; set; }
public bool RemoveFailedDownloads { get; set; }
}
public class DownloadClientResourceMapper : ProviderResourceMapper<DownloadClientResource, DownloadClientDefinition>
{
public override DownloadClientResource ToResource(DownloadClientDefinition definition)
{
if (definition == null)
{
return null;
}
var resource = base.ToResource(definition);
resource.Enable = definition.Enable;
resource.Protocol = definition.Protocol;
resource.Priority = definition.Priority;
resource.RemoveCompletedDownloads = definition.RemoveCompletedDownloads;
resource.RemoveFailedDownloads = definition.RemoveFailedDownloads;
return resource;
}
public override DownloadClientDefinition ToModel(DownloadClientResource resource)
{
if (resource == null)
{
return null;
}
var definition = base.ToModel(resource);
definition.Enable = resource.Enable;
definition.Protocol = resource.Protocol;
definition.Priority = resource.Priority;
definition.RemoveCompletedDownloads = resource.RemoveCompletedDownloads;
definition.RemoveFailedDownloads = resource.RemoveFailedDownloads;
return definition;
}
}
}

@ -0,0 +1,41 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.Extras.Subtitles;
using Radarr.Http;
namespace Radarr.Api.V4.ExtraFiles
{
[V4ApiController("extrafile")]
public class ExtraFileController : Controller
{
private readonly IExtraFileService<SubtitleFile> _subtitleFileService;
private readonly IExtraFileService<MetadataFile> _metadataFileService;
private readonly IExtraFileService<OtherExtraFile> _otherFileService;
public ExtraFileController(IExtraFileService<SubtitleFile> subtitleFileService, IExtraFileService<MetadataFile> metadataFileService, IExtraFileService<OtherExtraFile> otherExtraFileService)
{
_subtitleFileService = subtitleFileService;
_metadataFileService = metadataFileService;
_otherFileService = otherExtraFileService;
}
[HttpGet]
public List<ExtraFileResource> GetFiles(int movieId)
{
var extraFiles = new List<ExtraFileResource>();
List<SubtitleFile> subtitleFiles = _subtitleFileService.GetFilesByMovie(movieId);
List<MetadataFile> metadataFiles = _metadataFileService.GetFilesByMovie(movieId);
List<OtherExtraFile> otherExtraFiles = _otherFileService.GetFilesByMovie(movieId);
extraFiles.AddRange(subtitleFiles.ToResource());
extraFiles.AddRange(metadataFiles.ToResource());
extraFiles.AddRange(otherExtraFiles.ToResource());
return extraFiles;
}
}
}

@ -0,0 +1,91 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.Extras.Subtitles;
using Radarr.Http.REST;
namespace Radarr.Api.V4.ExtraFiles
{
public class ExtraFileResource : RestResource
{
public int MovieId { get; set; }
public int? MovieFileId { get; set; }
public string RelativePath { get; set; }
public string Extension { get; set; }
public ExtraFileType Type { get; set; }
}
public static class ExtraFileResourceMapper
{
public static ExtraFileResource ToResource(this MetadataFile model)
{
if (model == null)
{
return null;
}
return new ExtraFileResource
{
Id = model.Id,
MovieId = model.MovieId,
MovieFileId = model.MovieFileId,
RelativePath = model.RelativePath,
Extension = model.Extension,
Type = ExtraFileType.Metadata
};
}
public static ExtraFileResource ToResource(this SubtitleFile model)
{
if (model == null)
{
return null;
}
return new ExtraFileResource
{
Id = model.Id,
MovieId = model.MovieId,
MovieFileId = model.MovieFileId,
RelativePath = model.RelativePath,
Extension = model.Extension,
Type = ExtraFileType.Subtitle
};
}
public static ExtraFileResource ToResource(this OtherExtraFile model)
{
if (model == null)
{
return null;
}
return new ExtraFileResource
{
Id = model.Id,
MovieId = model.MovieId,
MovieFileId = model.MovieFileId,
RelativePath = model.RelativePath,
Extension = model.Extension,
Type = ExtraFileType.Other
};
}
public static List<ExtraFileResource> ToResource(this IEnumerable<SubtitleFile> movies)
{
return movies.Select(ToResource).ToList();
}
public static List<ExtraFileResource> ToResource(this IEnumerable<MetadataFile> movies)
{
return movies.Select(ToResource).ToList();
}
public static List<ExtraFileResource> ToResource(this IEnumerable<OtherExtraFile> movies)
{
return movies.Select(ToResource).ToList();
}
}
}

@ -0,0 +1,62 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles;
using Radarr.Http;
namespace Radarr.Api.V4.FileSystem
{
[V4ApiController]
public class FileSystemController : Controller
{
private readonly IFileSystemLookupService _fileSystemLookupService;
private readonly IDiskProvider _diskProvider;
private readonly IDiskScanService _diskScanService;
public FileSystemController(IFileSystemLookupService fileSystemLookupService,
IDiskProvider diskProvider,
IDiskScanService diskScanService)
{
_fileSystemLookupService = fileSystemLookupService;
_diskProvider = diskProvider;
_diskScanService = diskScanService;
}
[HttpGet]
public IActionResult GetContents(string path, bool includeFiles = false, bool allowFoldersWithoutTrailingSlashes = false)
{
return Ok(_fileSystemLookupService.LookupContents(path, includeFiles, allowFoldersWithoutTrailingSlashes));
}
[HttpGet("type")]
public object GetEntityType(string path)
{
if (_diskProvider.FileExists(path))
{
return new { type = "file" };
}
// Return folder even if it doesn't exist on disk to avoid leaking anything from the UI about the underlying system
return new { type = "folder" };
}
[HttpGet("mediafiles")]
public object GetMediaFiles(string path)
{
if (!_diskProvider.FolderExists(path))
{
return Array.Empty<string>();
}
return _diskScanService.GetVideoFiles(path).Select(f => new
{
Path = f,
RelativePath = path.GetRelativePath(f),
Name = Path.GetFileName(f)
});
}
}
}

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.SignalR;
using Radarr.Http;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Health
{
[V4ApiController]
public class HealthController : RestControllerWithSignalR<HealthResource, HealthCheck>,
IHandle<HealthCheckCompleteEvent>
{
private readonly IHealthCheckService _healthCheckService;
public HealthController(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService)
: base(signalRBroadcaster)
{
_healthCheckService = healthCheckService;
}
protected override HealthResource GetResourceById(int id)
{
throw new NotImplementedException();
}
[HttpGet]
public List<HealthResource> GetHealth()
{
return _healthCheckService.Results().ToResource();
}
[NonAction]
public void Handle(HealthCheckCompleteEvent message)
{
BroadcastResourceChange(ModelAction.Sync);
}
}
}

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Http;
using NzbDrone.Core.HealthCheck;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Health
{
public class HealthResource : RestResource
{
public string Source { get; set; }
public HealthCheckResult Type { get; set; }
public string Message { get; set; }
public HttpUri WikiUrl { get; set; }
}
public static class HealthResourceMapper
{
public static HealthResource ToResource(this HealthCheck model)
{
if (model == null)
{
return null;
}
return new HealthResource
{
Id = model.Id,
Source = model.Source.Name,
Type = model.Type,
Message = model.Message,
WikiUrl = model.WikiUrl
};
}
public static List<HealthResource> ToResource(this IEnumerable<HealthCheck> models)
{
return models.Select(ToResource).ToList();
}
}
}

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download;
using NzbDrone.Core.History;
using NzbDrone.Core.Movies;
using Radarr.Api.V4.Movies;
using Radarr.Http;
using Radarr.Http.Extensions;
namespace Radarr.Api.V4.History
{
[V4ApiController]
public class HistoryController : Controller
{
private readonly IHistoryService _historyService;
private readonly IMovieService _movieService;
private readonly ICustomFormatCalculationService _formatCalculator;
private readonly IUpgradableSpecification _upgradableSpecification;
private readonly IFailedDownloadService _failedDownloadService;
public HistoryController(IHistoryService historyService,
IMovieService movieService,
ICustomFormatCalculationService formatCalculator,
IUpgradableSpecification upgradableSpecification,
IFailedDownloadService failedDownloadService)
{
_historyService = historyService;
_movieService = movieService;
_formatCalculator = formatCalculator;
_upgradableSpecification = upgradableSpecification;
_failedDownloadService = failedDownloadService;
}
protected HistoryResource MapToResource(MovieHistory model, bool includeMovie)
{
if (model.Movie == null)
{
model.Movie = _movieService.GetMovie(model.MovieId);
}
var resource = model.ToResource(_formatCalculator);
if (includeMovie)
{
resource.Movie = model.Movie.ToResource(0);
}
if (model.Movie != null)
{
resource.QualityCutoffNotMet = _upgradableSpecification.QualityCutoffNotMet(model.Movie.Profile, model.Quality);
}
return resource;
}
[HttpGet]
public PagingResource<HistoryResource> GetHistory(bool includeMovie)
{
var pagingResource = Request.ReadPagingResourceFromRequest<HistoryResource>();
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, MovieHistory>("date", SortDirection.Descending);
var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType");
var downloadIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "downloadId");
if (eventTypeFilter != null)
{
var filterValue = (MovieHistoryEventType)Convert.ToInt32(eventTypeFilter.Value);
pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue);
}
if (downloadIdFilter != null)
{
var downloadId = downloadIdFilter.Value;
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
}
return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h, includeMovie));
}
[HttpGet("since")]
public List<HistoryResource> GetHistorySince(DateTime date, MovieHistoryEventType? eventType = null, bool includeMovie = false)
{
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeMovie)).ToList();
}
[HttpGet("movie")]
public List<HistoryResource> GetMovieHistory(int movieId, MovieHistoryEventType? eventType = null, bool includeMovie = false)
{
return _historyService.GetByMovieId(movieId, eventType).Select(h => MapToResource(h, includeMovie)).ToList();
}
[HttpPost("failed/{id}")]
public object MarkAsFailed([FromRoute] int id)
{
_failedDownloadService.MarkAsFailed(id);
return new { };
}
}
}

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.History;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Qualities;
using Radarr.Api.V4.CustomFormats;
using Radarr.Api.V4.Movies;
using Radarr.Http.REST;
namespace Radarr.Api.V4.History
{
public class HistoryResource : RestResource
{
public int MovieId { get; set; }
public string SourceTitle { get; set; }
public List<Language> Languages { get; set; }
public QualityModel Quality { get; set; }
public List<CustomFormatResource> CustomFormats { get; set; }
public bool QualityCutoffNotMet { get; set; }
public DateTime Date { get; set; }
public string DownloadId { get; set; }
public MovieHistoryEventType EventType { get; set; }
public Dictionary<string, string> Data { get; set; }
public MovieResource Movie { get; set; }
}
public static class HistoryResourceMapper
{
public static HistoryResource ToResource(this MovieHistory model, ICustomFormatCalculationService formatCalculator)
{
if (model == null)
{
return null;
}
return new HistoryResource
{
Id = model.Id,
MovieId = model.MovieId,
SourceTitle = model.SourceTitle,
Languages = model.Languages,
Quality = model.Quality,
CustomFormats = formatCalculator.ParseCustomFormat(model).ToResource(),
// QualityCutoffNotMet
Date = model.Date,
DownloadId = model.DownloadId,
EventType = model.EventType,
Data = model.Data
};
}
}
}

@ -0,0 +1,65 @@
using System.Collections.Generic;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.ImportLists.ImportExclusions;
using Radarr.Http;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V4.ImportLists
{
[V4ApiController("exclusions")]
public class ImportExclusionsController : RestController<ImportExclusionsResource>
{
private readonly IImportExclusionsService _exclusionService;
public ImportExclusionsController(IImportExclusionsService exclusionService)
{
_exclusionService = exclusionService;
SharedValidator.RuleFor(c => c.TmdbId).GreaterThan(0);
SharedValidator.RuleFor(c => c.MovieTitle).NotEmpty();
SharedValidator.RuleFor(c => c.MovieYear).GreaterThan(0);
}
[HttpGet]
public List<ImportExclusionsResource> GetAll()
{
return _exclusionService.GetAllExclusions().ToResource();
}
protected override ImportExclusionsResource GetResourceById(int id)
{
return _exclusionService.GetById(id).ToResource();
}
[RestPutById]
public ActionResult<ImportExclusionsResource> UpdateExclusion(ImportExclusionsResource exclusionResource)
{
var model = exclusionResource.ToModel();
return Accepted(_exclusionService.Update(model));
}
[RestPostById]
public ActionResult<ImportExclusionsResource> AddExclusion(ImportExclusionsResource exclusionResource)
{
var model = exclusionResource.ToModel();
return Created(_exclusionService.AddExclusion(model).Id);
}
[HttpPost("bulk")]
public object AddExclusions([FromBody] List<ImportExclusionsResource> resource)
{
var newMovies = resource.ToModel();
return _exclusionService.AddExclusions(newMovies).ToResource();
}
[RestDeleteById]
public void RemoveExclusion(int id)
{
_exclusionService.RemoveExclusion(new ImportExclusion { Id = id });
}
}
}

@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.ImportLists.ImportExclusions;
namespace Radarr.Api.V4.ImportLists
{
public class ImportExclusionsResource : ProviderResource<ImportExclusionsResource>
{
// public int Id { get; set; }
public int TmdbId { get; set; }
public string MovieTitle { get; set; }
public int MovieYear { get; set; }
}
public static class ImportExclusionsResourceMapper
{
public static ImportExclusionsResource ToResource(this ImportExclusion model)
{
if (model == null)
{
return null;
}
return new ImportExclusionsResource
{
Id = model.Id,
TmdbId = model.TmdbId,
MovieTitle = model.MovieTitle,
MovieYear = model.MovieYear
};
}
public static List<ImportExclusionsResource> ToResource(this IEnumerable<ImportExclusion> exclusions)
{
return exclusions.Select(ToResource).ToList();
}
public static ImportExclusion ToModel(this ImportExclusionsResource resource)
{
return new ImportExclusion
{
Id = resource.Id,
TmdbId = resource.TmdbId,
MovieTitle = resource.MovieTitle,
MovieYear = resource.MovieYear
};
}
public static List<ImportExclusion> ToModel(this IEnumerable<ImportExclusionsResource> resources)
{
return resources.Select(ToModel).ToList();
}
}
}

@ -0,0 +1,24 @@
using FluentValidation;
using NzbDrone.Core.ImportLists;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using Radarr.Http;
namespace Radarr.Api.V4.ImportLists
{
[V4ApiController]
public class ImportListController : ProviderControllerBase<ImportListResource, IImportList, ImportListDefinition>
{
public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper();
public ImportListController(IImportListFactory importListFactory,
ProfileExistsValidator profileExistsValidator)
: base(importListFactory, "importlist", ResourceMapper)
{
SharedValidator.RuleFor(c => c.RootFolderPath).IsValidPath();
SharedValidator.RuleFor(c => c.MinimumAvailability).NotNull();
SharedValidator.RuleForEach(c => c.QualityProfileIds).ValidId();
SharedValidator.RuleForEach(c => c.QualityProfileIds).SetValidator(profileExistsValidator);
}
}
}

@ -0,0 +1,158 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.ImportLists;
using NzbDrone.Core.ImportLists.ImportExclusions;
using NzbDrone.Core.ImportLists.ImportListMovies;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Organizer;
using Radarr.Api.V4.Movies;
using Radarr.Http;
namespace Radarr.Api.V4.ImportLists
{
[V4ApiController("importlist/movie")]
public class ImportListMoviesController : Controller
{
private readonly IMovieService _movieService;
private readonly IAddMovieService _addMovieService;
private readonly IProvideMovieInfo _movieInfo;
private readonly IBuildFileNames _fileNameBuilder;
private readonly IImportListMovieService _listMovieService;
private readonly IImportListFactory _importListFactory;
private readonly IImportExclusionsService _importExclusionService;
private readonly INamingConfigService _namingService;
private readonly IConfigService _configService;
public ImportListMoviesController(IMovieService movieService,
IAddMovieService addMovieService,
IProvideMovieInfo movieInfo,
IBuildFileNames fileNameBuilder,
IImportListMovieService listMovieService,
IImportListFactory importListFactory,
IImportExclusionsService importExclusionsService,
INamingConfigService namingService,
IConfigService configService)
{
_movieService = movieService;
_addMovieService = addMovieService;
_movieInfo = movieInfo;
_fileNameBuilder = fileNameBuilder;
_listMovieService = listMovieService;
_importListFactory = importListFactory;
_importExclusionService = importExclusionsService;
_namingService = namingService;
_configService = configService;
}
[HttpGet]
public object GetDiscoverMovies(bool includeRecommendations = false)
{
var movieLanguge = (Language)_configService.MovieInfoLanguage;
var realResults = new List<ImportListMoviesResource>();
var listExclusions = _importExclusionService.GetAllExclusions();
var existingTmdbIds = _movieService.AllMovieTmdbIds();
if (includeRecommendations)
{
var mapped = new List<Movie>();
var results = _movieService.GetRecommendedTmdbIds();
if (results.Count > 0)
{
mapped = _movieInfo.GetBulkMovieInfo(results).Select(m => new Movie { MovieMetadata = m }).ToList();
}
realResults.AddRange(MapToResource(mapped.Where(x => x != null), movieLanguge));
realResults.ForEach(x => x.IsRecommendation = true);
}
var listMovies = MapToResource(_listMovieService.GetAllForLists(_importListFactory.Enabled().Select(x => x.Definition.Id).ToList()), movieLanguge).ToList();
realResults.AddRange(listMovies);
var groupedListMovies = realResults.GroupBy(x => x.TmdbId);
// Distinct Movies
realResults = groupedListMovies.Select(x =>
{
var movie = x.First();
movie.Lists = x.SelectMany(m => m.Lists).ToHashSet();
movie.IsExcluded = listExclusions.Any(e => e.TmdbId == movie.TmdbId);
movie.IsExisting = existingTmdbIds.Any(e => e == movie.TmdbId);
movie.IsRecommendation = x.Any(m => m.IsRecommendation);
return movie;
}).ToList();
return realResults;
}
[HttpPost]
public object AddMovies([FromBody] List<MovieResource> resource)
{
var newMovies = resource.ToModel();
return _addMovieService.AddMovies(newMovies, true).ToResource(0);
}
private IEnumerable<ImportListMoviesResource> MapToResource(IEnumerable<Movie> movies, Language language)
{
// Avoid calling for naming spec on every movie in filenamebuilder
var namingConfig = _namingService.GetConfig();
foreach (var currentMovie in movies)
{
var resource = DiscoverMoviesResourceMapper.ToResource(currentMovie);
var poster = currentMovie.MovieMetadata.Value.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster);
if (poster != null)
{
resource.RemotePoster = poster.Url;
}
var translation = currentMovie.MovieMetadata.Value.Translations.FirstOrDefault(t => t.Language == language);
resource.Title = translation?.Title ?? resource.Title;
resource.Overview = translation?.Overview ?? resource.Overview;
resource.Folder = _fileNameBuilder.GetMovieFolder(currentMovie, namingConfig);
yield return resource;
}
}
private IEnumerable<ImportListMoviesResource> MapToResource(IEnumerable<ImportListMovie> movies, Language language)
{
// Avoid calling for naming spec on every movie in filenamebuilder
var namingConfig = _namingService.GetConfig();
foreach (var currentMovie in movies)
{
var resource = DiscoverMoviesResourceMapper.ToResource(currentMovie);
var poster = currentMovie.MovieMetadata.Value.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster);
if (poster != null)
{
resource.RemotePoster = poster.Url;
}
var translation = currentMovie.MovieMetadata.Value.Translations.FirstOrDefault(t => t.Language == language);
resource.Title = translation?.Title ?? resource.Title;
resource.Overview = translation?.Overview ?? resource.Overview;
resource.Folder = _fileNameBuilder.GetMovieFolder(new Movie
{
MovieMetadata = currentMovie.MovieMetadata
}, namingConfig);
yield return resource;
}
}
}
}

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.ImportLists.ImportListMovies;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Collections;
using Radarr.Http.REST;
namespace Radarr.Api.V4.ImportLists
{
public class ImportListMoviesResource : RestResource
{
public ImportListMoviesResource()
{
Lists = new HashSet<int>();
}
public string Title { get; set; }
public string SortTitle { get; set; }
public MovieStatusType Status { get; set; }
public string Overview { get; set; }
public DateTime? InCinemas { get; set; }
public DateTime? PhysicalRelease { get; set; }
public DateTime? DigitalRelease { get; set; }
public List<MediaCover> Images { get; set; }
public string Website { get; set; }
public string RemotePoster { get; set; }
public int Year { get; set; }
public string YouTubeTrailerId { get; set; }
public string Studio { get; set; }
public int Runtime { get; set; }
public string ImdbId { get; set; }
public int TmdbId { get; set; }
public string Folder { get; set; }
public string Certification { get; set; }
public List<string> Genres { get; set; }
public Ratings Ratings { get; set; }
public MovieCollection Collection { get; set; }
public bool IsExcluded { get; set; }
public bool IsExisting { get; set; }
public bool IsRecommendation { get; set; }
public HashSet<int> Lists { get; set; }
}
public static class DiscoverMoviesResourceMapper
{
public static ImportListMoviesResource ToResource(this Movie model)
{
if (model == null)
{
return null;
}
return new ImportListMoviesResource
{
TmdbId = model.TmdbId,
Title = model.Title,
SortTitle = model.MovieMetadata.Value.SortTitle,
InCinemas = model.MovieMetadata.Value.InCinemas,
PhysicalRelease = model.MovieMetadata.Value.PhysicalRelease,
DigitalRelease = model.MovieMetadata.Value.DigitalRelease,
Status = model.MovieMetadata.Value.Status,
Overview = model.MovieMetadata.Value.Overview,
Images = model.MovieMetadata.Value.Images,
Year = model.Year,
Runtime = model.MovieMetadata.Value.Runtime,
ImdbId = model.ImdbId,
Certification = model.MovieMetadata.Value.Certification,
Website = model.MovieMetadata.Value.Website,
Genres = model.MovieMetadata.Value.Genres,
Ratings = model.MovieMetadata.Value.Ratings,
YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId,
Collection = new MovieCollection { Title = model.MovieMetadata.Value.CollectionTitle, TmdbId = model.MovieMetadata.Value.CollectionTmdbId },
Studio = model.MovieMetadata.Value.Studio
};
}
public static ImportListMoviesResource ToResource(this ImportListMovie model)
{
if (model == null)
{
return null;
}
return new ImportListMoviesResource
{
TmdbId = model.TmdbId,
Title = model.Title,
SortTitle = model.MovieMetadata.Value.SortTitle,
InCinemas = model.MovieMetadata.Value.InCinemas,
PhysicalRelease = model.MovieMetadata.Value.PhysicalRelease,
DigitalRelease = model.MovieMetadata.Value.DigitalRelease,
Status = model.MovieMetadata.Value.Status,
Overview = model.MovieMetadata.Value.Overview,
Images = model.MovieMetadata.Value.Images,
Year = model.Year,
Runtime = model.MovieMetadata.Value.Runtime,
ImdbId = model.ImdbId,
Certification = model.MovieMetadata.Value.Certification,
Website = model.MovieMetadata.Value.Website,
Genres = model.MovieMetadata.Value.Genres,
Ratings = model.MovieMetadata.Value.Ratings,
YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId,
Studio = model.MovieMetadata.Value.Studio,
Collection = new MovieCollection { Title = model.MovieMetadata.Value.CollectionTitle, TmdbId = model.MovieMetadata.Value.CollectionTmdbId },
Lists = new HashSet<int> { model.ListId }
};
}
}
}

@ -0,0 +1,65 @@
using System.Collections.Generic;
using NzbDrone.Core.ImportLists;
using NzbDrone.Core.Movies;
namespace Radarr.Api.V4.ImportLists
{
public class ImportListResource : ProviderResource<ImportListResource>
{
public bool Enabled { get; set; }
public bool EnableAuto { get; set; }
public MonitorTypes Monitor { get; set; }
public string RootFolderPath { get; set; }
public List<int> QualityProfileIds { get; set; }
public bool SearchOnAdd { get; set; }
public MovieStatusType MinimumAvailability { get; set; }
public ImportListType ListType { get; set; }
public int ListOrder { get; set; }
}
public class ImportListResourceMapper : ProviderResourceMapper<ImportListResource, ImportListDefinition>
{
public override ImportListResource ToResource(ImportListDefinition definition)
{
if (definition == null)
{
return null;
}
var resource = base.ToResource(definition);
resource.Enabled = definition.Enabled;
resource.EnableAuto = definition.EnableAuto;
resource.Monitor = definition.Monitor;
resource.SearchOnAdd = definition.SearchOnAdd;
resource.RootFolderPath = definition.RootFolderPath;
resource.QualityProfileIds = definition.QualityProfileIds;
resource.MinimumAvailability = definition.MinimumAvailability;
resource.ListType = definition.ListType;
resource.ListOrder = (int)definition.ListType;
return resource;
}
public override ImportListDefinition ToModel(ImportListResource resource)
{
if (resource == null)
{
return null;
}
var definition = base.ToModel(resource);
definition.Enabled = resource.Enabled;
definition.EnableAuto = resource.EnableAuto;
definition.Monitor = resource.Monitor;
definition.SearchOnAdd = resource.SearchOnAdd;
definition.RootFolderPath = resource.RootFolderPath;
definition.QualityProfileIds = resource.QualityProfileIds;
definition.MinimumAvailability = resource.MinimumAvailability;
definition.ListType = resource.ListType;
return definition;
}
}
}

@ -0,0 +1,16 @@
using NzbDrone.Core.Indexers;
using Radarr.Http;
namespace Radarr.Api.V4.Indexers
{
[V4ApiController]
public class IndexerController : ProviderControllerBase<IndexerResource, IIndexer, IndexerDefinition>
{
public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper();
public IndexerController(IndexerFactory indexerFactory)
: base(indexerFactory, "indexer", ResourceMapper)
{
}
}
}

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Parser.Model;
using Radarr.Http;
namespace Radarr.Api.V4.Indexers
{
[V4ApiController]
public class IndexerFlagController : Controller
{
[HttpGet]
public List<IndexerFlagResource> GetAll()
{
return Enum.GetValues(typeof(IndexerFlags)).Cast<IndexerFlags>().Select(f => new IndexerFlagResource
{
Id = (int)f,
Name = f.ToString()
}).ToList();
}
}
}

@ -0,0 +1,13 @@
using Newtonsoft.Json;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Indexers
{
public class IndexerFlagResource : RestResource
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)]
public new int Id { get; set; }
public string Name { get; set; }
public string NameLower => Name.ToLowerInvariant();
}
}

@ -0,0 +1,58 @@
using NzbDrone.Core.Indexers;
namespace Radarr.Api.V4.Indexers
{
public class IndexerResource : ProviderResource<IndexerResource>
{
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public bool SupportsRss { get; set; }
public bool SupportsSearch { get; set; }
public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; }
public int DownloadClientId { get; set; }
}
public class IndexerResourceMapper : ProviderResourceMapper<IndexerResource, IndexerDefinition>
{
public override IndexerResource ToResource(IndexerDefinition definition)
{
if (definition == null)
{
return null;
}
var resource = base.ToResource(definition);
resource.EnableRss = definition.EnableRss;
resource.EnableAutomaticSearch = definition.EnableAutomaticSearch;
resource.EnableInteractiveSearch = definition.EnableInteractiveSearch;
resource.SupportsRss = definition.SupportsRss;
resource.SupportsSearch = definition.SupportsSearch;
resource.Protocol = definition.Protocol;
resource.Priority = definition.Priority;
resource.DownloadClientId = definition.DownloadClientId;
return resource;
}
public override IndexerDefinition ToModel(IndexerResource resource)
{
if (resource == null)
{
return null;
}
var definition = base.ToModel(resource);
definition.EnableRss = resource.EnableRss;
definition.EnableAutomaticSearch = resource.EnableAutomaticSearch;
definition.EnableInteractiveSearch = resource.EnableInteractiveSearch;
definition.Priority = resource.Priority;
definition.DownloadClientId = resource.DownloadClientId;
return definition;
}
}
}

@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Validation;
using Radarr.Http;
using HttpStatusCode = System.Net.HttpStatusCode;
namespace Radarr.Api.V4.Indexers
{
[V4ApiController]
public class ReleaseController : ReleaseControllerBase
{
private readonly IFetchAndParseRss _rssFetcherAndParser;
private readonly ISearchForReleases _releaseSearchService;
private readonly IMakeDownloadDecision _downloadDecisionMaker;
private readonly IPrioritizeDownloadDecision _prioritizeDownloadDecision;
private readonly IDownloadService _downloadService;
private readonly IMovieService _movieService;
private readonly Logger _logger;
private readonly ICached<RemoteMovie> _remoteMovieCache;
public ReleaseController(IFetchAndParseRss rssFetcherAndParser,
ISearchForReleases releaseSearchService,
IMakeDownloadDecision downloadDecisionMaker,
IPrioritizeDownloadDecision prioritizeDownloadDecision,
IDownloadService downloadService,
IMovieService movieService,
ICacheManager cacheManager,
IProfileService qualityProfileService,
Logger logger)
: base(qualityProfileService)
{
_rssFetcherAndParser = rssFetcherAndParser;
_releaseSearchService = releaseSearchService;
_downloadDecisionMaker = downloadDecisionMaker;
_prioritizeDownloadDecision = prioritizeDownloadDecision;
_downloadService = downloadService;
_movieService = movieService;
_logger = logger;
PostValidator.RuleFor(s => s.IndexerId).ValidId();
PostValidator.RuleFor(s => s.Guid).NotEmpty();
_remoteMovieCache = cacheManager.GetCache<RemoteMovie>(GetType(), "remoteMovies");
}
[HttpPost]
public object DownloadRelease(ReleaseResource release)
{
var remoteMovie = _remoteMovieCache.Find(GetCacheKey(release));
if (remoteMovie == null)
{
_logger.Debug("Couldn't find requested release in cache, cache timeout probably expired.");
throw new NzbDroneClientException(HttpStatusCode.NotFound, "Couldn't find requested release in cache, try searching again");
}
try
{
if (remoteMovie.Movie == null)
{
if (release.MovieId.HasValue)
{
var movie = _movieService.GetMovie(release.MovieId.Value);
remoteMovie.Movie = movie;
}
else
{
throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to find matching movie");
}
}
_downloadService.DownloadReport(remoteMovie);
}
catch (ReleaseDownloadException ex)
{
_logger.Error(ex, ex.Message);
throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed");
}
return release;
}
[HttpGet]
public List<ReleaseResource> GetReleases(int? movieId)
{
if (movieId.HasValue)
{
return GetMovieReleases(movieId.Value);
}
return GetRss();
}
private List<ReleaseResource> GetMovieReleases(int movieId)
{
try
{
var decisions = _releaseSearchService.MovieSearch(movieId, true, true);
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisionsForMovies(decisions);
return MapDecisions(prioritizedDecisions);
}
catch (SearchFailedException ex)
{
throw new NzbDroneClientException(HttpStatusCode.BadRequest, ex.Message);
}
catch (Exception ex)
{
_logger.Error(ex, "Movie search failed: " + ex.Message);
}
return new List<ReleaseResource>();
}
private List<ReleaseResource> GetRss()
{
var reports = _rssFetcherAndParser.Fetch();
var decisions = _downloadDecisionMaker.GetRssDecision(reports);
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisionsForMovies(decisions);
return MapDecisions(prioritizedDecisions);
}
protected override ReleaseResource MapDecision(DownloadDecision decision, int initialWeight)
{
var resource = base.MapDecision(decision, initialWeight);
_remoteMovieCache.Set(GetCacheKey(resource), decision.RemoteMovie, TimeSpan.FromMinutes(30));
return resource;
}
private string GetCacheKey(ReleaseResource resource)
{
return string.Concat(resource.IndexerId, "_", resource.Guid);
}
}
}

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Profiles;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Indexers
{
public abstract class ReleaseControllerBase : RestController<ReleaseResource>
{
private readonly Profile _qualityProfile;
public ReleaseControllerBase(IProfileService qualityProfileService)
{
_qualityProfile = qualityProfileService.GetDefaultProfile(string.Empty);
}
protected override ReleaseResource GetResourceById(int id)
{
throw new NotImplementedException();
}
protected virtual List<ReleaseResource> MapDecisions(IEnumerable<DownloadDecision> decisions)
{
var result = new List<ReleaseResource>();
foreach (var downloadDecision in decisions)
{
var release = MapDecision(downloadDecision, result.Count);
result.Add(release);
}
return result;
}
protected virtual ReleaseResource MapDecision(DownloadDecision decision, int initialWeight)
{
var release = decision.ToResource();
release.ReleaseWeight = initialWeight;
release.QualityWeight = _qualityProfile.GetIndex(release.Quality.Quality).Index * 100;
release.QualityWeight += release.Quality.Revision.Real * 10;
release.QualityWeight += release.Quality.Revision.Version;
return release;
}
}
}

@ -0,0 +1,105 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
using Radarr.Http;
namespace Radarr.Api.V4.Indexers
{
[V4ApiController("release/push")]
public class ReleasePushController : ReleaseControllerBase
{
private readonly IMakeDownloadDecision _downloadDecisionMaker;
private readonly IProcessDownloadDecisions _downloadDecisionProcessor;
private readonly IIndexerFactory _indexerFactory;
private readonly Logger _logger;
public ReleasePushController(IMakeDownloadDecision downloadDecisionMaker,
IProcessDownloadDecisions downloadDecisionProcessor,
IIndexerFactory indexerFactory,
IProfileService qualityProfileService,
Logger logger)
: base(qualityProfileService)
{
_downloadDecisionMaker = downloadDecisionMaker;
_downloadDecisionProcessor = downloadDecisionProcessor;
_indexerFactory = indexerFactory;
_logger = logger;
PostValidator.RuleFor(s => s.Title).NotEmpty();
PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty();
PostValidator.RuleFor(s => s.Protocol).NotEmpty();
PostValidator.RuleFor(s => s.PublishDate).NotEmpty();
}
[HttpPost]
public ActionResult<List<ReleaseResource>> Create(ReleaseResource release)
{
_logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl);
ValidateResource(release);
var info = release.ToModel();
info.Guid = "PUSH-" + info.DownloadUrl;
ResolveIndexer(info);
var decisions = _downloadDecisionMaker.GetRssDecision(new List<ReleaseInfo> { info });
_downloadDecisionProcessor.ProcessDecisions(decisions);
var firstDecision = decisions.FirstOrDefault();
if (firstDecision?.RemoteMovie.ParsedMovieInfo == null)
{
throw new ValidationException(new List<ValidationFailure> { new ValidationFailure("Title", "Unable to parse", release.Title) });
}
return MapDecisions(new[] { firstDecision });
}
private void ResolveIndexer(ReleaseInfo release)
{
if (release.IndexerId == 0 && release.Indexer.IsNotNullOrWhiteSpace())
{
var indexer = _indexerFactory.All().FirstOrDefault(v => v.Name == release.Indexer);
if (indexer != null)
{
release.IndexerId = indexer.Id;
_logger.Debug("Push Release {0} associated with indexer {1} - {2}.", release.Title, release.IndexerId, release.Indexer);
}
else
{
_logger.Debug("Push Release {0} not associated with known indexer {1}.", release.Title, release.Indexer);
}
}
else if (release.IndexerId != 0 && release.Indexer.IsNullOrWhiteSpace())
{
try
{
var indexer = _indexerFactory.Get(release.IndexerId);
release.Indexer = indexer.Name;
_logger.Debug("Push Release {0} associated with indexer {1} - {2}.", release.Title, release.IndexerId, release.Indexer);
}
catch (ModelNotFoundException)
{
_logger.Debug("Push Release {0} not associated with known indexer {1}.", release.Title, release.IndexerId);
release.IndexerId = 0;
}
}
else
{
_logger.Debug("Push Release {0} not associated with an indexer.", release.Title);
}
}
}
}

@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using Radarr.Api.V4.CustomFormats;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Indexers
{
public class ReleaseResource : RestResource
{
public string Guid { get; set; }
public QualityModel Quality { get; set; }
public List<CustomFormatResource> CustomFormats { get; set; }
public int CustomFormatScore { get; set; }
public int QualityWeight { get; set; }
public int Age { get; set; }
public double AgeHours { get; set; }
public double AgeMinutes { get; set; }
public long Size { get; set; }
public int IndexerId { get; set; }
public string Indexer { get; set; }
public string ReleaseGroup { get; set; }
public string SubGroup { get; set; }
public string ReleaseHash { get; set; }
public string Title { get; set; }
public bool SceneSource { get; set; }
public List<string> MovieTitles { get; set; }
public List<Language> Languages { get; set; }
public bool Approved { get; set; }
public bool TemporarilyRejected { get; set; }
public bool Rejected { get; set; }
public int TmdbId { get; set; }
public int ImdbId { get; set; }
public IEnumerable<string> Rejections { get; set; }
public DateTime PublishDate { get; set; }
public string CommentUrl { get; set; }
public string DownloadUrl { get; set; }
public string InfoUrl { get; set; }
public bool DownloadAllowed { get; set; }
public int ReleaseWeight { get; set; }
public IEnumerable<string> IndexerFlags { get; set; }
public string Edition { get; set; }
public string MagnetUrl { get; set; }
public string InfoHash { get; set; }
public int? Seeders { get; set; }
public int? Leechers { get; set; }
public DownloadProtocol Protocol { get; set; }
// Sent when queuing an unknown release
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int? MovieId { get; set; }
}
public static class ReleaseResourceMapper
{
public static ReleaseResource ToResource(this DownloadDecision model)
{
var releaseInfo = model.RemoteMovie.Release;
var parsedMovieInfo = model.RemoteMovie.ParsedMovieInfo;
var remoteMovie = model.RemoteMovie;
var torrentInfo = (model.RemoteMovie.Release as TorrentInfo) ?? new TorrentInfo();
var indexerFlags = torrentInfo.IndexerFlags.ToString().Split(new string[] { ", " }, StringSplitOptions.None).Where(x => x != "0");
// TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?)
return new ReleaseResource
{
Guid = releaseInfo.Guid,
Quality = parsedMovieInfo.Quality,
CustomFormats = remoteMovie.CustomFormats.ToResource(),
CustomFormatScore = remoteMovie.CustomFormatScore,
// QualityWeight
Age = releaseInfo.Age,
AgeHours = releaseInfo.AgeHours,
AgeMinutes = releaseInfo.AgeMinutes,
Size = releaseInfo.Size,
IndexerId = releaseInfo.IndexerId,
Indexer = releaseInfo.Indexer,
ReleaseGroup = parsedMovieInfo.ReleaseGroup,
ReleaseHash = parsedMovieInfo.ReleaseHash,
Title = releaseInfo.Title,
MovieTitles = parsedMovieInfo.MovieTitles,
Languages = parsedMovieInfo.Languages,
Approved = model.Approved,
TemporarilyRejected = model.TemporarilyRejected,
Rejected = model.Rejected,
TmdbId = releaseInfo.TmdbId,
ImdbId = releaseInfo.ImdbId,
Rejections = model.Rejections.Select(r => r.Reason).ToList(),
PublishDate = releaseInfo.PublishDate,
CommentUrl = releaseInfo.CommentUrl,
DownloadUrl = releaseInfo.DownloadUrl,
InfoUrl = releaseInfo.InfoUrl,
DownloadAllowed = remoteMovie.DownloadAllowed,
Edition = parsedMovieInfo.Edition,
// ReleaseWeight
MagnetUrl = torrentInfo.MagnetUrl,
InfoHash = torrentInfo.InfoHash,
Seeders = torrentInfo.Seeders,
Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null,
Protocol = releaseInfo.DownloadProtocol,
IndexerFlags = indexerFlags
};
}
public static ReleaseInfo ToModel(this ReleaseResource resource)
{
ReleaseInfo model;
if (resource.Protocol == DownloadProtocol.Torrent)
{
model = new TorrentInfo
{
MagnetUrl = resource.MagnetUrl,
InfoHash = resource.InfoHash,
Seeders = resource.Seeders,
Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null
};
}
else
{
model = new ReleaseInfo();
}
model.Guid = resource.Guid;
model.Title = resource.Title;
model.Size = resource.Size;
model.DownloadUrl = resource.DownloadUrl;
model.InfoUrl = resource.InfoUrl;
model.CommentUrl = resource.CommentUrl;
model.IndexerId = resource.IndexerId;
model.Indexer = resource.Indexer;
model.DownloadProtocol = resource.Protocol;
model.TmdbId = resource.TmdbId;
model.ImdbId = resource.ImdbId;
model.PublishDate = resource.PublishDate.ToUniversalTime();
return model;
}
}
}

@ -0,0 +1,29 @@
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Localization;
using Radarr.Http;
namespace Radarr.Api.V4.Localization
{
[V4ApiController]
public class LocalizationController : Controller
{
private readonly ILocalizationService _localizationService;
private readonly JsonSerializerOptions _serializerSettings;
public LocalizationController(ILocalizationService localizationService)
{
_localizationService = localizationService;
_serializerSettings = STJson.GetSerializerSettings();
_serializerSettings.DictionaryKeyPolicy = null;
_serializerSettings.PropertyNamingPolicy = null;
}
[HttpGet]
public string GetLocalizationDictionary()
{
return JsonSerializer.Serialize(_localizationService.GetLocalizationDictionary().ToResource(), _serializerSettings);
}
}
}

@ -0,0 +1,26 @@
using System.Collections.Generic;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Localization
{
public class LocalizationResource : RestResource
{
public Dictionary<string, string> Strings { get; set; }
}
public static class LocalizationResourceMapper
{
public static LocalizationResource ToResource(this Dictionary<string, string> localization)
{
if (localization == null)
{
return null;
}
return new LocalizationResource
{
Strings = localization,
};
}
}
}

@ -0,0 +1,67 @@
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Instrumentation;
using Radarr.Http;
using Radarr.Http.Extensions;
namespace Radarr.Api.V4.Logs
{
[V4ApiController]
public class LogController : Controller
{
private readonly ILogService _logService;
public LogController(ILogService logService)
{
_logService = logService;
}
[HttpGet]
public PagingResource<LogResource> GetLogs()
{
var pagingResource = Request.ReadPagingResourceFromRequest<LogResource>();
var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>();
if (pageSpec.SortKey == "time")
{
pageSpec.SortKey = "id";
}
var levelFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "level");
if (levelFilter != null)
{
switch (levelFilter.Value)
{
case "fatal":
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal");
break;
case "error":
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error");
break;
case "warn":
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn");
break;
case "info":
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info");
break;
case "debug":
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug");
break;
case "trace":
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug" || h.Level == "Trace");
break;
}
}
var response = pageSpec.ApplyToPage(_logService.Paged, LogResourceMapper.ToResource);
if (pageSpec.SortKey == "id")
{
response.SortKey = "time";
}
return response;
}
}
}

@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.IO;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using Radarr.Http;
namespace Radarr.Api.V4.Logs
{
[V4ApiController("log/file")]
public class LogFileController : LogFileControllerBase
{
private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider;
public LogFileController(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider,
IConfigFileProvider configFileProvider)
: base(diskProvider, configFileProvider, "")
{
_appFolderInfo = appFolderInfo;
_diskProvider = diskProvider;
}
protected override IEnumerable<string> GetLogFiles()
{
return _diskProvider.GetFiles(_appFolderInfo.GetLogFolder(), SearchOption.TopDirectoryOnly);
}
protected override string GetLogFilePath(string filename)
{
return Path.Combine(_appFolderInfo.GetLogFolder(), filename);
}
protected override string DownloadUrlRoot
{
get
{
return "logfile";
}
}
}
}

@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
namespace Radarr.Api.V4.Logs
{
public abstract class LogFileControllerBase : Controller
{
protected const string LOGFILE_ROUTE = @"/(?<filename>[-.a-zA-Z0-9]+?\.txt)";
protected string _resource;
private readonly IDiskProvider _diskProvider;
private readonly IConfigFileProvider _configFileProvider;
public LogFileControllerBase(IDiskProvider diskProvider,
IConfigFileProvider configFileProvider,
string resource)
{
_diskProvider = diskProvider;
_configFileProvider = configFileProvider;
_resource = resource;
}
[HttpGet]
public List<LogFileResource> GetLogFilesResponse()
{
var result = new List<LogFileResource>();
var files = GetLogFiles().ToList();
for (int i = 0; i < files.Count; i++)
{
var file = files[i];
var filename = Path.GetFileName(file);
result.Add(new LogFileResource
{
Id = i + 1,
Filename = filename,
LastWriteTime = _diskProvider.FileGetLastWrite(file),
ContentsUrl = string.Format("{0}/api/v1/{1}/{2}", _configFileProvider.UrlBase, _resource, filename),
DownloadUrl = string.Format("{0}/{1}/{2}", _configFileProvider.UrlBase, DownloadUrlRoot, filename)
});
}
return result.OrderByDescending(l => l.LastWriteTime).ToList();
}
[HttpGet(@"{filename:regex([[-.a-zA-Z0-9]]+?\.txt)}")]
public IActionResult GetLogFileResponse(string filename)
{
LogManager.Flush();
var filePath = GetLogFilePath(filename);
if (!_diskProvider.FileExists(filePath))
{
return NotFound();
}
return PhysicalFile(filePath, "text/plain");
}
protected abstract IEnumerable<string> GetLogFiles();
protected abstract string GetLogFilePath(string filename);
protected abstract string DownloadUrlRoot { get; }
}
}

@ -0,0 +1,13 @@
using System;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Logs
{
public class LogFileResource : RestResource
{
public string Filename { get; set; }
public DateTime LastWriteTime { get; set; }
public string ContentsUrl { get; set; }
public string DownloadUrl { get; set; }
}
}

@ -0,0 +1,39 @@
using System;
using NzbDrone.Core.Instrumentation;
using Radarr.Http.REST;
namespace Radarr.Api.V4.Logs
{
public class LogResource : RestResource
{
public DateTime Time { get; set; }
public string Exception { get; set; }
public string ExceptionType { get; set; }
public string Level { get; set; }
public string Logger { get; set; }
public string Message { get; set; }
public string Method { get; set; }
}
public static class LogResourceMapper
{
public static LogResource ToResource(this Log model)
{
if (model == null)
{
return null;
}
return new LogResource
{
Id = model.Id,
Time = model.Time,
Exception = model.Exception,
ExceptionType = model.ExceptionType,
Level = model.Level.ToLowerInvariant(),
Logger = model.Logger,
Message = model.Message
};
}
}
}

@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using Radarr.Http;
namespace Radarr.Api.V4.Logs
{
[V4ApiController("log/file/update")]
public class UpdateLogFileController : LogFileControllerBase
{
private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider;
public UpdateLogFileController(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider,
IConfigFileProvider configFileProvider)
: base(diskProvider, configFileProvider, "update")
{
_appFolderInfo = appFolderInfo;
_diskProvider = diskProvider;
}
protected override IEnumerable<string> GetLogFiles()
{
if (!_diskProvider.FolderExists(_appFolderInfo.GetUpdateLogFolder()))
{
return Enumerable.Empty<string>();
}
return _diskProvider.GetFiles(_appFolderInfo.GetUpdateLogFolder(), SearchOption.TopDirectoryOnly)
.Where(f => Regex.IsMatch(Path.GetFileName(f), LOGFILE_ROUTE.TrimStart('/'), RegexOptions.IgnoreCase))
.ToList();
}
protected override string GetLogFilePath(string filename)
{
return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), filename);
}
protected override string DownloadUrlRoot
{
get
{
return "updatelogfile";
}
}
}
}

@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles.MovieImport.Manual;
using NzbDrone.Core.Qualities;
using Radarr.Api.V4.Movies;
using Radarr.Http;
namespace Radarr.Api.V4.ManualImport
{
[V4ApiController]
public class ManualImportController : Controller
{
private readonly IManualImportService _manualImportService;
public ManualImportController(IManualImportService manualImportService)
{
_manualImportService = manualImportService;
}
[HttpGet]
public List<ManualImportResource> GetMediaFiles(string folder, string downloadId, int? movieId, bool filterExistingFiles = true)
{
return _manualImportService.GetMediaFiles(folder, downloadId, movieId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList();
}
[HttpPost]
public object ReprocessItems([FromBody] List<ManualImportReprocessResource> items)
{
foreach (var item in items)
{
var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.MovieId, item.ReleaseGroup, item.Quality, item.Languages);
item.Movie = processedItem.Movie.ToResource(0);
item.Rejections = processedItem.Rejections;
if (item.Languages.Single() == Language.Unknown)
{
item.Languages = processedItem.Languages;
}
if (item.Quality?.Quality == Quality.Unknown)
{
item.Quality = processedItem.Quality;
}
if (item.ReleaseGroup.IsNotNullOrWhiteSpace())
{
item.ReleaseGroup = processedItem.ReleaseGroup;
}
}
return items;
}
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;
}
}
}

@ -0,0 +1,22 @@
using System.Collections.Generic;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Qualities;
using Radarr.Api.V4.Movies;
using Radarr.Http.REST;
namespace Radarr.Api.V4.ManualImport
{
public class ManualImportReprocessResource : RestResource
{
public string Path { get; set; }
public int MovieId { get; set; }
public MovieResource Movie { get; set; }
public QualityModel Quality { get; set; }
public List<Language> Languages { get; set; }
public string ReleaseGroup { get; set; }
public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; }
}
}

@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Crypto;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles.MovieImport.Manual;
using NzbDrone.Core.Qualities;
using Radarr.Api.V4.Movies;
using Radarr.Http.REST;
namespace Radarr.Api.V4.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 MovieResource Movie { get; set; }
public QualityModel Quality { get; set; }
public List<Language> Languages { get; set; }
public string ReleaseGroup { get; set; }
public int QualityWeight { get; set; }
public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; }
}
public static class ManualImportResourceMapper
{
public static ManualImportResource ToResource(this ManualImportItem model)
{
if (model == null)
{
return null;
}
return new ManualImportResource
{
Id = HashConverter.GetHashInt31(model.Path),
Path = model.Path,
RelativePath = model.RelativePath,
FolderName = model.FolderName,
Name = model.Name,
Size = model.Size,
Movie = model.Movie.ToResource(0),
Quality = model.Quality,
Languages = model.Languages,
ReleaseGroup = model.ReleaseGroup,
// QualityWeight
DownloadId = model.DownloadId,
Rejections = model.Rejections
};
}
public static List<ManualImportResource> ToResource(this IEnumerable<ManualImportItem> models)
{
return models.Select(ToResource).ToList();
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save