mirror of
https://github.com/Radarr/Radarr.git
synced 2025-04-24 06:27:08 -04:00
New: v4 API (DROP v3 AFTER TESTING PERIOD)
This commit is contained in:
parent
5b2f30227b
commit
5dc3726023
167 changed files with 21718 additions and 58 deletions
|
@ -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: |
|
||||
|
|
2
docs.sh
2
docs.sh
|
@ -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
|
||||
|
@ -275,6 +276,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,
|
||||
|
|
9
src/Radarr.Api.V4/Blocklist/BlocklistBulkResource.cs
Normal file
9
src/Radarr.Api.V4/Blocklist/BlocklistBulkResource.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Radarr.Api.V4.Blocklist
|
||||
{
|
||||
public class BlocklistBulkResource
|
||||
{
|
||||
public List<int> Ids { get; set; }
|
||||
}
|
||||
}
|
55
src/Radarr.Api.V4/Blocklist/BlocklistController.cs
Normal file
55
src/Radarr.Api.V4/Blocklist/BlocklistController.cs
Normal file
|
@ -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 { };
|
||||
}
|
||||
}
|
||||
}
|
55
src/Radarr.Api.V4/Blocklist/BlocklistResource.cs
Normal file
55
src/Radarr.Api.V4/Blocklist/BlocklistResource.cs
Normal file
|
@ -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, model.Movie).ToResource(false),
|
||||
Date = model.Date,
|
||||
Protocol = model.Protocol,
|
||||
Indexer = model.Indexer,
|
||||
Message = model.Message,
|
||||
|
||||
Movie = model.Movie.ToResource(0)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
83
src/Radarr.Api.V4/Calendar/CalendarController.cs
Normal file
83
src/Radarr.Api.V4/Calendar/CalendarController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
106
src/Radarr.Api.V4/Calendar/CalendarFeedController.cs
Normal file
106
src/Radarr.Api.V4/Calendar/CalendarFeedController.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
188
src/Radarr.Api.V4/Collections/CollectionController.cs
Normal file
188
src/Radarr.Api.V4/Collections/CollectionController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
53
src/Radarr.Api.V4/Collections/CollectionMovieResource.cs
Normal file
53
src/Radarr.Api.V4/Collections/CollectionMovieResource.cs
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
91
src/Radarr.Api.V4/Collections/CollectionResource.cs
Normal file
91
src/Radarr.Api.V4/Collections/CollectionResource.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
16
src/Radarr.Api.V4/Collections/CollectionUpdateResource.cs
Normal file
16
src/Radarr.Api.V4/Collections/CollectionUpdateResource.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
125
src/Radarr.Api.V4/Commands/CommandController.cs
Normal file
125
src/Radarr.Api.V4/Commands/CommandController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
124
src/Radarr.Api.V4/Commands/CommandResource.cs
Normal file
124
src/Radarr.Api.V4/Commands/CommandResource.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
48
src/Radarr.Api.V4/Config/ConfigController.cs
Normal file
48
src/Radarr.Api.V4/Config/ConfigController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
19
src/Radarr.Api.V4/Config/DownloadClientConfigController.cs
Normal file
19
src/Radarr.Api.V4/Config/DownloadClientConfigController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
31
src/Radarr.Api.V4/Config/DownloadClientConfigResource.cs
Normal file
31
src/Radarr.Api.V4/Config/DownloadClientConfigResource.cs
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
121
src/Radarr.Api.V4/Config/HostConfigController.cs
Normal file
121
src/Radarr.Api.V4/Config/HostConfigController.cs
Normal file
|
@ -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)
|
||||
.ValidIpAddress()
|
||||
.NotListenAllIp4Address()
|
||||
.When(c => c.BindAddress != "*" && c.BindAddress != "localhost");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
93
src/Radarr.Api.V4/Config/HostConfigResource.cs
Normal file
93
src/Radarr.Api.V4/Config/HostConfigResource.cs
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
23
src/Radarr.Api.V4/Config/ImportListConfigController.cs
Normal file
23
src/Radarr.Api.V4/Config/ImportListConfigController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
25
src/Radarr.Api.V4/Config/ImportListConfigResource.cs
Normal file
25
src/Radarr.Api.V4/Config/ImportListConfigResource.cs
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
32
src/Radarr.Api.V4/Config/IndexerConfigController.cs
Normal file
32
src/Radarr.Api.V4/Config/IndexerConfigController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
35
src/Radarr.Api.V4/Config/IndexerConfigResource.cs
Normal file
35
src/Radarr.Api.V4/Config/IndexerConfigResource.cs
Normal file
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
45
src/Radarr.Api.V4/Config/MediaManagementConfigController.cs
Normal file
45
src/Radarr.Api.V4/Config/MediaManagementConfigController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
62
src/Radarr.Api.V4/Config/MediaManagementConfigResource.cs
Normal file
62
src/Radarr.Api.V4/Config/MediaManagementConfigResource.cs
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
19
src/Radarr.Api.V4/Config/MetadataConfigController.cs
Normal file
19
src/Radarr.Api.V4/Config/MetadataConfigController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
22
src/Radarr.Api.V4/Config/MetadataConfigResource.cs
Normal file
22
src/Radarr.Api.V4/Config/MetadataConfigResource.cs
Normal file
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
105
src/Radarr.Api.V4/Config/NamingConfigController.cs
Normal file
105
src/Radarr.Api.V4/Config/NamingConfigController.cs
Normal file
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
src/Radarr.Api.V4/Config/NamingConfigResource.cs
Normal file
18
src/Radarr.Api.V4/Config/NamingConfigResource.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
54
src/Radarr.Api.V4/Config/NamingExampleResource.cs
Normal file
54
src/Radarr.Api.V4/Config/NamingExampleResource.cs
Normal file
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
39
src/Radarr.Api.V4/Config/UiConfigController.cs
Normal file
39
src/Radarr.Api.V4/Config/UiConfigController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
50
src/Radarr.Api.V4/Config/UiConfigResource.cs
Normal file
50
src/Radarr.Api.V4/Config/UiConfigResource.cs
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
44
src/Radarr.Api.V4/Credits/CreditController.cs
Normal file
44
src/Radarr.Api.V4/Credits/CreditController.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
80
src/Radarr.Api.V4/Credits/CreditResource.cs
Normal file
80
src/Radarr.Api.V4/Credits/CreditResource.cs
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
52
src/Radarr.Api.V4/CustomFilters/CustomFilterController.cs
Normal file
52
src/Radarr.Api.V4/CustomFilters/CustomFilterController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
56
src/Radarr.Api.V4/CustomFilters/CustomFilterResource.cs
Normal file
56
src/Radarr.Api.V4/CustomFilters/CustomFilterResource.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
170
src/Radarr.Api.V4/CustomFormats/CustomFormatController.cs
Normal file
170
src/Radarr.Api.V4/CustomFormats/CustomFormatController.cs
Normal file
|
@ -0,0 +1,170 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
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(true);
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<CustomFormatResource> Create(CustomFormatResource customFormatResource)
|
||||
{
|
||||
var model = customFormatResource.ToModel(_specifications);
|
||||
|
||||
Validate(model);
|
||||
|
||||
return Created(_formatService.Insert(model).Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<CustomFormatResource> Update(CustomFormatResource resource)
|
||||
{
|
||||
var model = resource.ToModel(_specifications);
|
||||
|
||||
Validate(model);
|
||||
|
||||
_formatService.Update(model);
|
||||
|
||||
return Accepted(model.Id);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public List<CustomFormatResource> GetAll()
|
||||
{
|
||||
return _formatService.All().ToResource(true);
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
75
src/Radarr.Api.V4/CustomFormats/CustomFormatResource.cs
Normal file
75
src/Radarr.Api.V4/CustomFormats/CustomFormatResource.cs
Normal file
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
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, bool includeDetails)
|
||||
{
|
||||
var resource = new CustomFormatResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name
|
||||
};
|
||||
|
||||
if (includeDetails)
|
||||
{
|
||||
resource.IncludeCustomFormatWhenRenaming = model.IncludeCustomFormatWhenRenaming;
|
||||
resource.Specifications = model.Specifications.Select(x => x.ToSchema()).ToList();
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
public static List<CustomFormatResource> ToResource(this IEnumerable<CustomFormat> models, bool includeDetails)
|
||||
{
|
||||
return models.Select(m => m.ToResource(includeDetails)).ToList();
|
||||
}
|
||||
|
||||
public static CustomFormat ToModel(this CustomFormatResource resource, List<ICustomFormatSpecification> specifications)
|
||||
{
|
||||
return new CustomFormat
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
IncludeCustomFormatWhenRenaming = resource.IncludeCustomFormatWhenRenaming ?? false,
|
||||
Specifications = resource.Specifications?.Select(x => MapSpecification(x, specifications)).ToList() ?? new List<ICustomFormatSpecification>()
|
||||
};
|
||||
}
|
||||
|
||||
private static ICustomFormatSpecification MapSpecification(CustomFormatSpecificationSchema resource, List<ICustomFormatSpecification> specifications)
|
||||
{
|
||||
var matchingSpec =
|
||||
specifications.SingleOrDefault(x => x.GetType().Name == resource.Implementation);
|
||||
|
||||
if (matchingSpec is null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{resource.Implementation} is not a valid specification implementation");
|
||||
}
|
||||
|
||||
var type = matchingSpec.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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
24
src/Radarr.Api.V4/DiskSpace/DiskSpaceController.cs
Normal file
24
src/Radarr.Api.V4/DiskSpace/DiskSpaceController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
31
src/Radarr.Api.V4/DiskSpace/DiskSpaceResource.cs
Normal file
31
src/Radarr.Api.V4/DiskSpace/DiskSpaceResource.cs
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
16
src/Radarr.Api.V4/DownloadClient/DownloadClientController.cs
Normal file
16
src/Radarr.Api.V4/DownloadClient/DownloadClientController.cs
Normal file
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
53
src/Radarr.Api.V4/DownloadClient/DownloadClientResource.cs
Normal file
53
src/Radarr.Api.V4/DownloadClient/DownloadClientResource.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
41
src/Radarr.Api.V4/ExtraFiles/ExtraFileController.cs
Normal file
41
src/Radarr.Api.V4/ExtraFiles/ExtraFileController.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
91
src/Radarr.Api.V4/ExtraFiles/ExtraFileResource.cs
Normal file
91
src/Radarr.Api.V4/ExtraFiles/ExtraFileResource.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
62
src/Radarr.Api.V4/FileSystem/FileSystemController.cs
Normal file
62
src/Radarr.Api.V4/FileSystem/FileSystemController.cs
Normal file
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
42
src/Radarr.Api.V4/Health/HealthController.cs
Normal file
42
src/Radarr.Api.V4/Health/HealthController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
41
src/Radarr.Api.V4/Health/HealthResource.cs
Normal file
41
src/Radarr.Api.V4/Health/HealthResource.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
104
src/Radarr.Api.V4/History/HistoryController.cs
Normal file
104
src/Radarr.Api.V4/History/HistoryController.cs
Normal file
|
@ -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 { };
|
||||
}
|
||||
}
|
||||
}
|
65
src/Radarr.Api.V4/History/HistoryResource.cs
Normal file
65
src/Radarr.Api.V4/History/HistoryResource.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
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 int CustomFormatScore { 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;
|
||||
}
|
||||
|
||||
var customFormats = formatCalculator.ParseCustomFormat(model, model.Movie);
|
||||
var customFormatScore = model.Movie.Profile.CalculateCustomFormatScore(customFormats);
|
||||
|
||||
return new HistoryResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
MovieId = model.MovieId,
|
||||
SourceTitle = model.SourceTitle,
|
||||
Languages = model.Languages,
|
||||
Quality = model.Quality,
|
||||
CustomFormats = customFormats.ToResource(false),
|
||||
CustomFormatScore = customFormatScore,
|
||||
|
||||
// QualityCutoffNotMet
|
||||
Date = model.Date,
|
||||
DownloadId = model.DownloadId,
|
||||
|
||||
EventType = model.EventType,
|
||||
|
||||
Data = model.Data
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
65
src/Radarr.Api.V4/ImportLists/ImportExclusionsController.cs
Normal file
65
src/Radarr.Api.V4/ImportLists/ImportExclusionsController.cs
Normal file
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
54
src/Radarr.Api.V4/ImportLists/ImportExclusionsResource.cs
Normal file
54
src/Radarr.Api.V4/ImportLists/ImportExclusionsResource.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
24
src/Radarr.Api.V4/ImportLists/ImportListController.cs
Normal file
24
src/Radarr.Api.V4/ImportLists/ImportListController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
158
src/Radarr.Api.V4/ImportLists/ImportListMoviesController.cs
Normal file
158
src/Radarr.Api.V4/ImportLists/ImportListMoviesController.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
120
src/Radarr.Api.V4/ImportLists/ImportListMoviesResource.cs
Normal file
120
src/Radarr.Api.V4/ImportLists/ImportListMoviesResource.cs
Normal file
|
@ -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 }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
65
src/Radarr.Api.V4/ImportLists/ImportListResource.cs
Normal file
65
src/Radarr.Api.V4/ImportLists/ImportListResource.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
16
src/Radarr.Api.V4/Indexers/IndexerController.cs
Normal file
16
src/Radarr.Api.V4/Indexers/IndexerController.cs
Normal file
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
23
src/Radarr.Api.V4/Indexers/IndexerFlagController.cs
Normal file
23
src/Radarr.Api.V4/Indexers/IndexerFlagController.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
13
src/Radarr.Api.V4/Indexers/IndexerFlagResource.cs
Normal file
13
src/Radarr.Api.V4/Indexers/IndexerFlagResource.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
58
src/Radarr.Api.V4/Indexers/IndexerResource.cs
Normal file
58
src/Radarr.Api.V4/Indexers/IndexerResource.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
152
src/Radarr.Api.V4/Indexers/ReleaseController.cs
Normal file
152
src/Radarr.Api.V4/Indexers/ReleaseController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
51
src/Radarr.Api.V4/Indexers/ReleaseControllerBase.cs
Normal file
51
src/Radarr.Api.V4/Indexers/ReleaseControllerBase.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
113
src/Radarr.Api.V4/Indexers/ReleasePushController.cs
Normal file
113
src/Radarr.Api.V4/Indexers/ReleasePushController.cs
Normal file
|
@ -0,0 +1,113 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
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;
|
||||
|
||||
private static readonly object PushLock = new object();
|
||||
|
||||
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);
|
||||
|
||||
List<DownloadDecision> decisions;
|
||||
|
||||
lock (PushLock)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
149
src/Radarr.Api.V4/Indexers/ReleaseResource.cs
Normal file
149
src/Radarr.Api.V4/Indexers/ReleaseResource.cs
Normal file
|
@ -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(false),
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
29
src/Radarr.Api.V4/Localization/LocalizationController.cs
Normal file
29
src/Radarr.Api.V4/Localization/LocalizationController.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
26
src/Radarr.Api.V4/Localization/LocalizationResource.cs
Normal file
26
src/Radarr.Api.V4/Localization/LocalizationResource.cs
Normal file
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
67
src/Radarr.Api.V4/Logs/LogController.cs
Normal file
67
src/Radarr.Api.V4/Logs/LogController.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
44
src/Radarr.Api.V4/Logs/LogFileController.cs
Normal file
44
src/Radarr.Api.V4/Logs/LogFileController.cs
Normal file
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
73
src/Radarr.Api.V4/Logs/LogFileControllerBase.cs
Normal file
73
src/Radarr.Api.V4/Logs/LogFileControllerBase.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
13
src/Radarr.Api.V4/Logs/LogFileResource.cs
Normal file
13
src/Radarr.Api.V4/Logs/LogFileResource.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
39
src/Radarr.Api.V4/Logs/LogResource.cs
Normal file
39
src/Radarr.Api.V4/Logs/LogResource.cs
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
53
src/Radarr.Api.V4/Logs/UpdateLogFileController.cs
Normal file
53
src/Radarr.Api.V4/Logs/UpdateLogFileController.cs
Normal file
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
69
src/Radarr.Api.V4/ManualImport/ManualImportController.cs
Normal file
69
src/Radarr.Api.V4/ManualImport/ManualImportController.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
62
src/Radarr.Api.V4/ManualImport/ManualImportResource.cs
Normal file
62
src/Radarr.Api.V4/ManualImport/ManualImportResource.cs
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue