From 5dc37260231e02df4bf672185bf1ed27ab706a2a Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 10 Sep 2022 14:53:35 -0500 Subject: [PATCH] New: v4 API (DROP v3 AFTER TESTING PERIOD) --- azure-pipelines.yml | 2 +- docs.sh | 2 +- .../Calendar/iCal/CalendarLinkModalContent.js | 2 +- src/NzbDrone.Host/Bootstrap.cs | 1 + src/NzbDrone.Host/Radarr.Host.csproj | 1 + src/NzbDrone.Host/Startup.cs | 8 +- .../ApiTests/BlocklistFixture.cs | 4 +- .../ApiTests/CalendarFixture.cs | 2 +- .../ApiTests/DiskSpaceFixture.cs | 2 +- .../ApiTests/IndexerFixture.cs | 2 +- .../ApiTests/MovieEditorFixture.cs | 2 +- .../ApiTests/ReleaseFixture.cs | 2 +- .../ApiTests/RootFolderFixture.cs | 2 +- .../ApiTests/TagFixture.cs | 2 +- .../Client/DownloadClientClient.cs | 2 +- .../Client/IndexerClient.cs | 2 +- .../Client/MovieClient.cs | 2 +- .../Client/MovieFileClient.cs | 4 +- .../Client/NotificationClient.cs | 2 +- .../Client/ReleaseClient.cs | 2 +- .../HttpLogFixture.cs | 8 +- .../IntegrationTest.cs | 2 +- .../IntegrationTestBase.cs | 22 +- .../Radarr.Integration.Test.csproj | 2 +- src/NzbDrone.Test.Common/NzbDroneRunner.cs | 2 +- src/Radarr.Api.V3/Movies/MovieController.cs | 7 +- src/Radarr.Api.V3/Movies/MovieResource.cs | 14 +- .../Blocklist/BlocklistBulkResource.cs | 9 + .../Blocklist/BlocklistController.cs | 55 + .../Blocklist/BlocklistResource.cs | 55 + .../Calendar/CalendarController.cs | 83 + .../Calendar/CalendarFeedController.cs | 106 + .../Collections/CollectionController.cs | 188 + .../Collections/CollectionMovieResource.cs | 53 + .../Collections/CollectionResource.cs | 91 + .../CollectionUpdateCollectionResource.cs | 14 + .../Collections/CollectionUpdateResource.cs | 16 + .../Commands/CommandController.cs | 125 + src/Radarr.Api.V4/Commands/CommandResource.cs | 124 + src/Radarr.Api.V4/Config/ConfigController.cs | 48 + .../Config/DownloadClientConfigController.cs | 19 + .../Config/DownloadClientConfigResource.cs | 31 + .../Config/HostConfigController.cs | 121 + .../Config/HostConfigResource.cs | 93 + .../Config/ImportListConfigController.cs | 23 + .../Config/ImportListConfigResource.cs | 25 + .../Config/IndexerConfigController.cs | 32 + .../Config/IndexerConfigResource.cs | 35 + .../Config/MediaManagementConfigController.cs | 45 + .../Config/MediaManagementConfigResource.cs | 62 + .../Config/MetadataConfigController.cs | 19 + .../Config/MetadataConfigResource.cs | 22 + .../Config/NamingConfigController.cs | 105 + .../Config/NamingConfigResource.cs | 18 + .../Config/NamingExampleResource.cs | 54 + .../Config/UiConfigController.cs | 39 + src/Radarr.Api.V4/Config/UiConfigResource.cs | 50 + src/Radarr.Api.V4/Credits/CreditController.cs | 44 + src/Radarr.Api.V4/Credits/CreditResource.cs | 80 + .../CustomFilters/CustomFilterController.cs | 52 + .../CustomFilters/CustomFilterResource.cs | 56 + .../CustomFormats/CustomFormatController.cs | 170 + .../CustomFormats/CustomFormatResource.cs | 75 + .../CustomFormatSpecificationSchema.cs | 36 + .../DiskSpace/DiskSpaceController.cs | 24 + .../DiskSpace/DiskSpaceResource.cs | 31 + .../DownloadClientController.cs | 16 + .../DownloadClient/DownloadClientResource.cs | 53 + .../ExtraFiles/ExtraFileController.cs | 41 + .../ExtraFiles/ExtraFileResource.cs | 91 + .../FileSystem/FileSystemController.cs | 62 + src/Radarr.Api.V4/Health/HealthController.cs | 42 + src/Radarr.Api.V4/Health/HealthResource.cs | 41 + .../History/HistoryController.cs | 104 + src/Radarr.Api.V4/History/HistoryResource.cs | 65 + .../ImportLists/ImportExclusionsController.cs | 65 + .../ImportLists/ImportExclusionsResource.cs | 54 + .../ImportLists/ImportListController.cs | 24 + .../ImportLists/ImportListMoviesController.cs | 158 + .../ImportLists/ImportListMoviesResource.cs | 120 + .../ImportLists/ImportListResource.cs | 65 + .../Indexers/IndexerController.cs | 16 + .../Indexers/IndexerFlagController.cs | 23 + .../Indexers/IndexerFlagResource.cs | 13 + src/Radarr.Api.V4/Indexers/IndexerResource.cs | 58 + .../Indexers/ReleaseController.cs | 152 + .../Indexers/ReleaseControllerBase.cs | 51 + .../Indexers/ReleasePushController.cs | 113 + src/Radarr.Api.V4/Indexers/ReleaseResource.cs | 149 + .../Localization/LocalizationController.cs | 29 + .../Localization/LocalizationResource.cs | 26 + src/Radarr.Api.V4/Logs/LogController.cs | 67 + src/Radarr.Api.V4/Logs/LogFileController.cs | 44 + .../Logs/LogFileControllerBase.cs | 73 + src/Radarr.Api.V4/Logs/LogFileResource.cs | 13 + src/Radarr.Api.V4/Logs/LogResource.cs | 39 + .../Logs/UpdateLogFileController.cs | 53 + .../ManualImport/ManualImportController.cs | 69 + .../ManualImportReprocessResource.cs | 22 + .../ManualImport/ManualImportResource.cs | 62 + .../MediaCovers/MediaCoverController.cs | 59 + .../Metadata/MetadataController.cs | 16 + .../Metadata/MetadataResource.cs | 40 + .../MovieFiles/MediaInfoResource.cs | 64 + .../MovieFiles/MovieFileController.cs | 204 + .../MovieFiles/MovieFileListResource.cs | 18 + .../MovieFiles/MovieFileResource.cs | 93 + .../Movies/AlternativeTitleController.cs | 45 + .../Movies/AlternativeTitleResource.cs | 76 + src/Radarr.Api.V4/Movies/MovieController.cs | 364 + .../Movies/MovieEditorController.cs | 100 + .../Movies/MovieEditorResource.cs | 26 + .../MovieFolderAsRootFolderValidator.cs | 54 + .../Movies/MovieImportController.cs | 33 + .../Movies/MovieLookupController.cs | 92 + src/Radarr.Api.V4/Movies/MovieResource.cs | 220 + .../Movies/MovieStatisticsResource.cs | 30 + .../Movies/RenameMovieController.cs | 25 + .../Movies/RenameMovieResource.cs | 39 + .../Notifications/NotificationController.cs | 16 + .../Notifications/NotificationResource.cs | 102 + src/Radarr.Api.V4/Parse/ParseController.cs | 62 + src/Radarr.Api.V4/Parse/ParseResource.cs | 13 + .../Profiles/Delay/DelayProfileController.cs | 75 + .../Profiles/Delay/DelayProfileResource.cs | 72 + .../Profiles/Languages/LanguageController.cs | 38 + .../Profiles/Languages/LanguageResource.cs | 13 + .../Quality/QualityCutoffValidator.cs | 44 + .../Profiles/Quality/QualityItemsValidator.cs | 189 + .../Quality/QualityProfileController.cs | 82 + .../Quality/QualityProfileResource.cs | 144 + .../Quality/QualityProfileSchemaController.cs | 25 + src/Radarr.Api.V4/ProviderControllerBase.cs | 209 + src/Radarr.Api.V4/ProviderResource.cs | 72 + src/Radarr.Api.V4/ProviderTestAllResult.cs | 18 + .../Qualities/QualityDefinitionController.cs | 54 + .../Qualities/QualityDefinitionResource.cs | 79 + .../Queue/QueueActionController.cs | 55 + src/Radarr.Api.V4/Queue/QueueBulkResource.cs | 9 + src/Radarr.Api.V4/Queue/QueueController.cs | 288 + .../Queue/QueueDetailsController.cs | 61 + src/Radarr.Api.V4/Queue/QueueResource.cs | 79 + .../Queue/QueueStatusController.cs | 79 + .../Queue/QueueStatusResource.cs | 15 + src/Radarr.Api.V4/Radarr.Api.V4.csproj | 14 + .../RemotePathMappingController.cs | 70 + .../RemotePathMappingResource.cs | 56 + .../Restrictions/RestrictionController.cs | 60 + .../Restrictions/RestrictionResource.cs | 64 + .../RootFolders/RootFolderController.cs | 71 + .../RootFolders/RootFolderResource.cs | 62 + .../System/Backup/BackupController.cs | 133 + .../System/Backup/BackupResource.cs | 15 + src/Radarr.Api.V4/System/SystemController.cs | 126 + .../System/Tasks/TaskController.cs | 69 + .../System/Tasks/TaskResource.cs | 17 + src/Radarr.Api.V4/Tags/TagController.cs | 61 + .../Tags/TagDetailsController.cs | 30 + src/Radarr.Api.V4/Tags/TagDetailsResource.cs | 46 + src/Radarr.Api.V4/Tags/TagResource.cs | 48 + src/Radarr.Api.V4/Update/UpdateController.cs | 64 + src/Radarr.Api.V4/Update/UpdateResource.cs | 56 + src/Radarr.Api.V4/openapi.json | 12685 ++++++++++++++++ .../Frontend/InitializeJsController.cs | 2 +- .../VersionedApiControllerAttribute.cs | 8 + .../VersionedFeedControllerAttribute.cs | 8 + src/Radarr.sln | 14 +- 167 files changed, 21718 insertions(+), 58 deletions(-) create mode 100644 src/Radarr.Api.V4/Blocklist/BlocklistBulkResource.cs create mode 100644 src/Radarr.Api.V4/Blocklist/BlocklistController.cs create mode 100644 src/Radarr.Api.V4/Blocklist/BlocklistResource.cs create mode 100644 src/Radarr.Api.V4/Calendar/CalendarController.cs create mode 100644 src/Radarr.Api.V4/Calendar/CalendarFeedController.cs create mode 100644 src/Radarr.Api.V4/Collections/CollectionController.cs create mode 100644 src/Radarr.Api.V4/Collections/CollectionMovieResource.cs create mode 100644 src/Radarr.Api.V4/Collections/CollectionResource.cs create mode 100644 src/Radarr.Api.V4/Collections/CollectionUpdateCollectionResource.cs create mode 100644 src/Radarr.Api.V4/Collections/CollectionUpdateResource.cs create mode 100644 src/Radarr.Api.V4/Commands/CommandController.cs create mode 100644 src/Radarr.Api.V4/Commands/CommandResource.cs create mode 100644 src/Radarr.Api.V4/Config/ConfigController.cs create mode 100644 src/Radarr.Api.V4/Config/DownloadClientConfigController.cs create mode 100644 src/Radarr.Api.V4/Config/DownloadClientConfigResource.cs create mode 100644 src/Radarr.Api.V4/Config/HostConfigController.cs create mode 100644 src/Radarr.Api.V4/Config/HostConfigResource.cs create mode 100644 src/Radarr.Api.V4/Config/ImportListConfigController.cs create mode 100644 src/Radarr.Api.V4/Config/ImportListConfigResource.cs create mode 100644 src/Radarr.Api.V4/Config/IndexerConfigController.cs create mode 100644 src/Radarr.Api.V4/Config/IndexerConfigResource.cs create mode 100644 src/Radarr.Api.V4/Config/MediaManagementConfigController.cs create mode 100644 src/Radarr.Api.V4/Config/MediaManagementConfigResource.cs create mode 100644 src/Radarr.Api.V4/Config/MetadataConfigController.cs create mode 100644 src/Radarr.Api.V4/Config/MetadataConfigResource.cs create mode 100644 src/Radarr.Api.V4/Config/NamingConfigController.cs create mode 100644 src/Radarr.Api.V4/Config/NamingConfigResource.cs create mode 100644 src/Radarr.Api.V4/Config/NamingExampleResource.cs create mode 100644 src/Radarr.Api.V4/Config/UiConfigController.cs create mode 100644 src/Radarr.Api.V4/Config/UiConfigResource.cs create mode 100644 src/Radarr.Api.V4/Credits/CreditController.cs create mode 100644 src/Radarr.Api.V4/Credits/CreditResource.cs create mode 100644 src/Radarr.Api.V4/CustomFilters/CustomFilterController.cs create mode 100644 src/Radarr.Api.V4/CustomFilters/CustomFilterResource.cs create mode 100644 src/Radarr.Api.V4/CustomFormats/CustomFormatController.cs create mode 100644 src/Radarr.Api.V4/CustomFormats/CustomFormatResource.cs create mode 100644 src/Radarr.Api.V4/CustomFormats/CustomFormatSpecificationSchema.cs create mode 100644 src/Radarr.Api.V4/DiskSpace/DiskSpaceController.cs create mode 100644 src/Radarr.Api.V4/DiskSpace/DiskSpaceResource.cs create mode 100644 src/Radarr.Api.V4/DownloadClient/DownloadClientController.cs create mode 100644 src/Radarr.Api.V4/DownloadClient/DownloadClientResource.cs create mode 100644 src/Radarr.Api.V4/ExtraFiles/ExtraFileController.cs create mode 100644 src/Radarr.Api.V4/ExtraFiles/ExtraFileResource.cs create mode 100644 src/Radarr.Api.V4/FileSystem/FileSystemController.cs create mode 100644 src/Radarr.Api.V4/Health/HealthController.cs create mode 100644 src/Radarr.Api.V4/Health/HealthResource.cs create mode 100644 src/Radarr.Api.V4/History/HistoryController.cs create mode 100644 src/Radarr.Api.V4/History/HistoryResource.cs create mode 100644 src/Radarr.Api.V4/ImportLists/ImportExclusionsController.cs create mode 100644 src/Radarr.Api.V4/ImportLists/ImportExclusionsResource.cs create mode 100644 src/Radarr.Api.V4/ImportLists/ImportListController.cs create mode 100644 src/Radarr.Api.V4/ImportLists/ImportListMoviesController.cs create mode 100644 src/Radarr.Api.V4/ImportLists/ImportListMoviesResource.cs create mode 100644 src/Radarr.Api.V4/ImportLists/ImportListResource.cs create mode 100644 src/Radarr.Api.V4/Indexers/IndexerController.cs create mode 100644 src/Radarr.Api.V4/Indexers/IndexerFlagController.cs create mode 100644 src/Radarr.Api.V4/Indexers/IndexerFlagResource.cs create mode 100644 src/Radarr.Api.V4/Indexers/IndexerResource.cs create mode 100644 src/Radarr.Api.V4/Indexers/ReleaseController.cs create mode 100644 src/Radarr.Api.V4/Indexers/ReleaseControllerBase.cs create mode 100644 src/Radarr.Api.V4/Indexers/ReleasePushController.cs create mode 100644 src/Radarr.Api.V4/Indexers/ReleaseResource.cs create mode 100644 src/Radarr.Api.V4/Localization/LocalizationController.cs create mode 100644 src/Radarr.Api.V4/Localization/LocalizationResource.cs create mode 100644 src/Radarr.Api.V4/Logs/LogController.cs create mode 100644 src/Radarr.Api.V4/Logs/LogFileController.cs create mode 100644 src/Radarr.Api.V4/Logs/LogFileControllerBase.cs create mode 100644 src/Radarr.Api.V4/Logs/LogFileResource.cs create mode 100644 src/Radarr.Api.V4/Logs/LogResource.cs create mode 100644 src/Radarr.Api.V4/Logs/UpdateLogFileController.cs create mode 100644 src/Radarr.Api.V4/ManualImport/ManualImportController.cs create mode 100644 src/Radarr.Api.V4/ManualImport/ManualImportReprocessResource.cs create mode 100644 src/Radarr.Api.V4/ManualImport/ManualImportResource.cs create mode 100644 src/Radarr.Api.V4/MediaCovers/MediaCoverController.cs create mode 100644 src/Radarr.Api.V4/Metadata/MetadataController.cs create mode 100644 src/Radarr.Api.V4/Metadata/MetadataResource.cs create mode 100644 src/Radarr.Api.V4/MovieFiles/MediaInfoResource.cs create mode 100644 src/Radarr.Api.V4/MovieFiles/MovieFileController.cs create mode 100644 src/Radarr.Api.V4/MovieFiles/MovieFileListResource.cs create mode 100644 src/Radarr.Api.V4/MovieFiles/MovieFileResource.cs create mode 100644 src/Radarr.Api.V4/Movies/AlternativeTitleController.cs create mode 100644 src/Radarr.Api.V4/Movies/AlternativeTitleResource.cs create mode 100644 src/Radarr.Api.V4/Movies/MovieController.cs create mode 100644 src/Radarr.Api.V4/Movies/MovieEditorController.cs create mode 100644 src/Radarr.Api.V4/Movies/MovieEditorResource.cs create mode 100644 src/Radarr.Api.V4/Movies/MovieFolderAsRootFolderValidator.cs create mode 100644 src/Radarr.Api.V4/Movies/MovieImportController.cs create mode 100644 src/Radarr.Api.V4/Movies/MovieLookupController.cs create mode 100644 src/Radarr.Api.V4/Movies/MovieResource.cs create mode 100644 src/Radarr.Api.V4/Movies/MovieStatisticsResource.cs create mode 100644 src/Radarr.Api.V4/Movies/RenameMovieController.cs create mode 100644 src/Radarr.Api.V4/Movies/RenameMovieResource.cs create mode 100644 src/Radarr.Api.V4/Notifications/NotificationController.cs create mode 100644 src/Radarr.Api.V4/Notifications/NotificationResource.cs create mode 100644 src/Radarr.Api.V4/Parse/ParseController.cs create mode 100644 src/Radarr.Api.V4/Parse/ParseResource.cs create mode 100644 src/Radarr.Api.V4/Profiles/Delay/DelayProfileController.cs create mode 100644 src/Radarr.Api.V4/Profiles/Delay/DelayProfileResource.cs create mode 100644 src/Radarr.Api.V4/Profiles/Languages/LanguageController.cs create mode 100644 src/Radarr.Api.V4/Profiles/Languages/LanguageResource.cs create mode 100644 src/Radarr.Api.V4/Profiles/Quality/QualityCutoffValidator.cs create mode 100644 src/Radarr.Api.V4/Profiles/Quality/QualityItemsValidator.cs create mode 100644 src/Radarr.Api.V4/Profiles/Quality/QualityProfileController.cs create mode 100644 src/Radarr.Api.V4/Profiles/Quality/QualityProfileResource.cs create mode 100644 src/Radarr.Api.V4/Profiles/Quality/QualityProfileSchemaController.cs create mode 100644 src/Radarr.Api.V4/ProviderControllerBase.cs create mode 100644 src/Radarr.Api.V4/ProviderResource.cs create mode 100644 src/Radarr.Api.V4/ProviderTestAllResult.cs create mode 100644 src/Radarr.Api.V4/Qualities/QualityDefinitionController.cs create mode 100644 src/Radarr.Api.V4/Qualities/QualityDefinitionResource.cs create mode 100644 src/Radarr.Api.V4/Queue/QueueActionController.cs create mode 100644 src/Radarr.Api.V4/Queue/QueueBulkResource.cs create mode 100644 src/Radarr.Api.V4/Queue/QueueController.cs create mode 100644 src/Radarr.Api.V4/Queue/QueueDetailsController.cs create mode 100644 src/Radarr.Api.V4/Queue/QueueResource.cs create mode 100644 src/Radarr.Api.V4/Queue/QueueStatusController.cs create mode 100644 src/Radarr.Api.V4/Queue/QueueStatusResource.cs create mode 100644 src/Radarr.Api.V4/Radarr.Api.V4.csproj create mode 100644 src/Radarr.Api.V4/RemotePathMappings/RemotePathMappingController.cs create mode 100644 src/Radarr.Api.V4/RemotePathMappings/RemotePathMappingResource.cs create mode 100644 src/Radarr.Api.V4/Restrictions/RestrictionController.cs create mode 100644 src/Radarr.Api.V4/Restrictions/RestrictionResource.cs create mode 100644 src/Radarr.Api.V4/RootFolders/RootFolderController.cs create mode 100644 src/Radarr.Api.V4/RootFolders/RootFolderResource.cs create mode 100644 src/Radarr.Api.V4/System/Backup/BackupController.cs create mode 100644 src/Radarr.Api.V4/System/Backup/BackupResource.cs create mode 100644 src/Radarr.Api.V4/System/SystemController.cs create mode 100644 src/Radarr.Api.V4/System/Tasks/TaskController.cs create mode 100644 src/Radarr.Api.V4/System/Tasks/TaskResource.cs create mode 100644 src/Radarr.Api.V4/Tags/TagController.cs create mode 100644 src/Radarr.Api.V4/Tags/TagDetailsController.cs create mode 100644 src/Radarr.Api.V4/Tags/TagDetailsResource.cs create mode 100644 src/Radarr.Api.V4/Tags/TagResource.cs create mode 100644 src/Radarr.Api.V4/Update/UpdateController.cs create mode 100644 src/Radarr.Api.V4/Update/UpdateResource.cs create mode 100644 src/Radarr.Api.V4/openapi.json diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dc8c7f8157..3275f85807 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -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: | diff --git a/docs.sh b/docs.sh index e4a8c87e82..1fa5b609aa 100644 --- a/docs.sh +++ b/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 diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js index 3f85a36734..11bfca4a1e 100644 --- a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js +++ b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js @@ -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&'; diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 37aa8cbc07..2509fe81dc 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -38,6 +38,7 @@ public static class Bootstrap "Radarr.Core", "Radarr.SignalR", "Radarr.Api.V3", + "Radarr.Api.V4", "Radarr.Http" }; diff --git a/src/NzbDrone.Host/Radarr.Host.csproj b/src/NzbDrone.Host/Radarr.Host.csproj index 920208614e..a3be591b86 100644 --- a/src/NzbDrone.Host/Radarr.Host.csproj +++ b/src/NzbDrone.Host/Radarr.Host.csproj @@ -15,6 +15,7 @@ + diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index ef1a568b5e..366b8f73aa 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -24,7 +24,7 @@ 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 @@ public void ConfigureServices(IServiceCollection services) 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 @@ public void ConfigureServices(IServiceCollection services) 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 @@ public void Configure(IApplicationBuilder app, app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(new List { "/api/v3/command" }); + app.UseMiddleware(new List { "/api/v4/command" }); app.UseWebSockets(); diff --git a/src/NzbDrone.Integration.Test/ApiTests/BlocklistFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/BlocklistFixture.cs index 64c0164606..b6deb197e7 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/BlocklistFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/BlocklistFixture.cs @@ -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 @@ public void should_be_able_to_add_to_blocklist() { _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" diff --git a/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs index 5f3a180c12..dadf0cb9d3 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs @@ -4,7 +4,7 @@ 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 { diff --git a/src/NzbDrone.Integration.Test/ApiTests/DiskSpaceFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/DiskSpaceFixture.cs index 3eabc8b790..fa3fcdda9c 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/DiskSpaceFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/DiskSpaceFixture.cs @@ -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 { diff --git a/src/NzbDrone.Integration.Test/ApiTests/IndexerFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/IndexerFixture.cs index 9acb6a5a38..9ec90a0d27 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/IndexerFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/IndexerFixture.cs @@ -3,7 +3,7 @@ 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 diff --git a/src/NzbDrone.Integration.Test/ApiTests/MovieEditorFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/MovieEditorFixture.cs index 209a36a901..48c7f2cbc9 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/MovieEditorFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/MovieEditorFixture.cs @@ -3,7 +3,7 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Test.Common; -using Radarr.Api.V3.Movies; +using Radarr.Api.V4.Movies; namespace NzbDrone.Integration.Test.ApiTests { diff --git a/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs index 8c4fdfb3f4..8411fb6a7b 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs @@ -2,7 +2,7 @@ using System.Net; using FluentAssertions; using NUnit.Framework; -using Radarr.Api.V3.Indexers; +using Radarr.Api.V4.Indexers; namespace NzbDrone.Integration.Test.ApiTests { diff --git a/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs index 9d65ab4112..dffeba5705 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs @@ -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 { diff --git a/src/NzbDrone.Integration.Test/ApiTests/TagFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/TagFixture.cs index 15fc061cb1..15da0f1a36 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/TagFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/TagFixture.cs @@ -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 { diff --git a/src/NzbDrone.Integration.Test/Client/DownloadClientClient.cs b/src/NzbDrone.Integration.Test/Client/DownloadClientClient.cs index 976a4b1c7a..1472cf14e4 100644 --- a/src/NzbDrone.Integration.Test/Client/DownloadClientClient.cs +++ b/src/NzbDrone.Integration.Test/Client/DownloadClientClient.cs @@ -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 diff --git a/src/NzbDrone.Integration.Test/Client/IndexerClient.cs b/src/NzbDrone.Integration.Test/Client/IndexerClient.cs index 190ae13bbc..3545c64511 100644 --- a/src/NzbDrone.Integration.Test/Client/IndexerClient.cs +++ b/src/NzbDrone.Integration.Test/Client/IndexerClient.cs @@ -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 diff --git a/src/NzbDrone.Integration.Test/Client/MovieClient.cs b/src/NzbDrone.Integration.Test/Client/MovieClient.cs index 87f10925c1..96a62a06f5 100644 --- a/src/NzbDrone.Integration.Test/Client/MovieClient.cs +++ b/src/NzbDrone.Integration.Test/Client/MovieClient.cs @@ -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 diff --git a/src/NzbDrone.Integration.Test/Client/MovieFileClient.cs b/src/NzbDrone.Integration.Test/Client/MovieFileClient.cs index 52686ccabc..ce0f19a69a 100644 --- a/src/NzbDrone.Integration.Test/Client/MovieFileClient.cs +++ b/src/NzbDrone.Integration.Test/Client/MovieFileClient.cs @@ -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 diff --git a/src/NzbDrone.Integration.Test/Client/NotificationClient.cs b/src/NzbDrone.Integration.Test/Client/NotificationClient.cs index 80ed367bc7..51c6a93481 100644 --- a/src/NzbDrone.Integration.Test/Client/NotificationClient.cs +++ b/src/NzbDrone.Integration.Test/Client/NotificationClient.cs @@ -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 diff --git a/src/NzbDrone.Integration.Test/Client/ReleaseClient.cs b/src/NzbDrone.Integration.Test/Client/ReleaseClient.cs index f502717a3f..15398a37d2 100644 --- a/src/NzbDrone.Integration.Test/Client/ReleaseClient.cs +++ b/src/NzbDrone.Integration.Test/Client/ReleaseClient.cs @@ -1,4 +1,4 @@ -using Radarr.Api.V3.Indexers; +using Radarr.Api.V4.Indexers; using RestSharp; namespace NzbDrone.Integration.Test.Client diff --git a/src/NzbDrone.Integration.Test/HttpLogFixture.cs b/src/NzbDrone.Integration.Test/HttpLogFixture.cs index 10a16a71dc..db1bf45ba6 100644 --- a/src/NzbDrone.Integration.Test/HttpLogFixture.cs +++ b/src/NzbDrone.Integration.Test/HttpLogFixture.cs @@ -20,15 +20,15 @@ public void should_log_on_error() 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")); } } } diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index 6dc3671e50..4cd6896150 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -53,7 +53,7 @@ protected override void InitializeTestTarget() // 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, diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index d9afb3091b..f94e16ed9e 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -17,16 +17,16 @@ 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 @@ public void SmokeTestSetup() 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); diff --git a/src/NzbDrone.Integration.Test/Radarr.Integration.Test.csproj b/src/NzbDrone.Integration.Test/Radarr.Integration.Test.csproj index cddca40c28..24929a9f19 100644 --- a/src/NzbDrone.Integration.Test/Radarr.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/Radarr.Integration.Test.csproj @@ -8,6 +8,6 @@ - + diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index 57f339e0e3..7b2320e2c7 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -31,7 +31,7 @@ public class NzbDroneRunner 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; diff --git a/src/Radarr.Api.V3/Movies/MovieController.cs b/src/Radarr.Api.V3/Movies/MovieController.cs index a065766f02..63b583d32e 100644 --- a/src/Radarr.Api.V3/Movies/MovieController.cs +++ b/src/Radarr.Api.V3/Movies/MovieController.cs @@ -82,7 +82,7 @@ public MovieController(IBroadcastSignalRMessage signalRBroadcaster, _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 @@ public MovieController(IBroadcastSignalRMessage signalRBroadcaster, .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 @@ private void LinkMovieStatistics(List resources, List 0; } [RestPostById] diff --git a/src/Radarr.Api.V3/Movies/MovieResource.cs b/src/Radarr.Api.V3/Movies/MovieResource.cs index ffbc2a1ef9..85d2fa0b0b 100644 --- a/src/Radarr.Api.V3/Movies/MovieResource.cs +++ b/src/Radarr.Api.V3/Movies/MovieResource.cs @@ -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 @@ public MovieResource() { Monitored = true; MinimumAvailability = MovieStatusType.Released; - QualityProfileIds = new List(); } // Todo: Sorters should be done completely on the client @@ -51,10 +51,10 @@ public MovieResource() // View & Edit public string Path { get; set; } - public List 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 @@ public static MovieResource ToResource(this Movie model, int availDelay, MovieTr SecondaryYear = model.MovieMetadata.Value.SecondaryYear, Path = model.Path, - QualityProfileIds = model.QualityProfileIds, QualityProfileId = model.QualityProfileIds.FirstOrDefault(), Monitored = model.Monitored, @@ -151,13 +150,6 @@ public static Movie ToModel(this MovieResource resource) 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 @@ public static Movie ToModel(this MovieResource resource) }, Path = resource.Path, - QualityProfileIds = resource.QualityProfileIds, + QualityProfileIds = new List { resource.QualityProfileId }, Monitored = resource.Monitored, MinimumAvailability = resource.MinimumAvailability, diff --git a/src/Radarr.Api.V4/Blocklist/BlocklistBulkResource.cs b/src/Radarr.Api.V4/Blocklist/BlocklistBulkResource.cs new file mode 100644 index 0000000000..4eca8d2314 --- /dev/null +++ b/src/Radarr.Api.V4/Blocklist/BlocklistBulkResource.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Radarr.Api.V4.Blocklist +{ + public class BlocklistBulkResource + { + public List Ids { get; set; } + } +} diff --git a/src/Radarr.Api.V4/Blocklist/BlocklistController.cs b/src/Radarr.Api.V4/Blocklist/BlocklistController.cs new file mode 100644 index 0000000000..63798d3b5d --- /dev/null +++ b/src/Radarr.Api.V4/Blocklist/BlocklistController.cs @@ -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 GetBlocklist() + { + var pagingResource = Request.ReadPagingResourceFromRequest(); + var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); + + return pagingSpec.ApplyToPage(_blocklistService.Paged, model => BlocklistResourceMapper.MapToResource(model, _formatCalculator)); + } + + [HttpGet("movie")] + public List 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 { }; + } + } +} diff --git a/src/Radarr.Api.V4/Blocklist/BlocklistResource.cs b/src/Radarr.Api.V4/Blocklist/BlocklistResource.cs new file mode 100644 index 0000000000..85b3bf6626 --- /dev/null +++ b/src/Radarr.Api.V4/Blocklist/BlocklistResource.cs @@ -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 Languages { get; set; } + public QualityModel Quality { get; set; } + public List 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) + }; + } + } +} diff --git a/src/Radarr.Api.V4/Calendar/CalendarController.cs b/src/Radarr.Api.V4/Calendar/CalendarController.cs new file mode 100644 index 0000000000..1a58ff3e26 --- /dev/null +++ b/src/Radarr.Api.V4/Calendar/CalendarController.cs @@ -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 + { + 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 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 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); + } + } +} diff --git a/src/Radarr.Api.V4/Calendar/CalendarFeedController.cs b/src/Radarr.Api.V4/Calendar/CalendarFeedController.cs new file mode 100644 index 0000000000..d14c9739cf --- /dev/null +++ b/src/Radarr.Api.V4/Calendar/CalendarFeedController.cs @@ -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(); + + 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(); + 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() { movie.Studio }; + + occurrence.Summary = $"{movie.Title} " + summaryText; + } + } +} diff --git a/src/Radarr.Api.V4/Collections/CollectionController.cs b/src/Radarr.Api.V4/Collections/CollectionController.cs new file mode 100644 index 0000000000..13b5408169 --- /dev/null +++ b/src/Radarr.Api.V4/Collections/CollectionController.cs @@ -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, + IHandle, + IHandle, + IHandle + { + 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 GetCollections(int? tmdbId) + { + var collectionResources = new List(); + + 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 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 MapToResource(List 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); + } + } +} diff --git a/src/Radarr.Api.V4/Collections/CollectionMovieResource.cs b/src/Radarr.Api.V4/Collections/CollectionMovieResource.cs new file mode 100644 index 0000000000..f386ded63b --- /dev/null +++ b/src/Radarr.Api.V4/Collections/CollectionMovieResource.cs @@ -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 Images { get; set; } + public int Year { get; set; } + public Ratings Ratings { get; set; } + public List 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 + }; + } + } +} diff --git a/src/Radarr.Api.V4/Collections/CollectionResource.cs b/src/Radarr.Api.V4/Collections/CollectionResource.cs new file mode 100644 index 0000000000..fbcc2bc17d --- /dev/null +++ b/src/Radarr.Api.V4/Collections/CollectionResource.cs @@ -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(); + } + + public string Title { get; set; } + public string SortTitle { get; set; } + public int TmdbId { get; set; } + public List Images { get; set; } + public string Overview { get; set; } + public bool Monitored { get; set; } + public string RootFolderPath { get; set; } + public List QualityProfileIds { get; set; } + public bool SearchOnAdd { get; set; } + public MovieStatusType MinimumAvailability { get; set; } + public List 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 ToResource(this IEnumerable 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; + } + } +} diff --git a/src/Radarr.Api.V4/Collections/CollectionUpdateCollectionResource.cs b/src/Radarr.Api.V4/Collections/CollectionUpdateCollectionResource.cs new file mode 100644 index 0000000000..70b4752159 --- /dev/null +++ b/src/Radarr.Api.V4/Collections/CollectionUpdateCollectionResource.cs @@ -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; } + } +} diff --git a/src/Radarr.Api.V4/Collections/CollectionUpdateResource.cs b/src/Radarr.Api.V4/Collections/CollectionUpdateResource.cs new file mode 100644 index 0000000000..0ff2eb7525 --- /dev/null +++ b/src/Radarr.Api.V4/Collections/CollectionUpdateResource.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Movies; + +namespace Radarr.Api.V4.Collections +{ + public class CollectionUpdateResource + { + public List CollectionIds { get; set; } + public bool? Monitored { get; set; } + public bool? MonitorMovies { get; set; } + public List QualityProfileIds { get; set; } + public string RootFolderPath { get; set; } + public MovieStatusType? MinimumAvailability { get; set; } + } +} diff --git a/src/Radarr.Api.V4/Commands/CommandController.cs b/src/Radarr.Api.V4/Commands/CommandController.cs new file mode 100644 index 0000000000..8b5ff1ea43 --- /dev/null +++ b/src/Radarr.Api.V4/Commands/CommandController.cs @@ -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, IHandle + { + private readonly IManageCommandQueue _commandQueueManager; + private readonly KnownTypes _knownTypes; + private readonly Debouncer _debouncer; + private readonly Dictionary _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(); + + PostValidator.RuleFor(c => c.Name).NotBlank(); + } + + protected override CommandResource GetResourceById(int id) + { + return _commandQueueManager.Get(id).ToResource(); + } + + [RestPostById] + public ActionResult 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 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); + } + } + } + } + } +} diff --git a/src/Radarr.Api.V4/Commands/CommandResource.cs b/src/Radarr.Api.V4/Commands/CommandResource.cs new file mode 100644 index 0000000000..46384e8d87 --- /dev/null +++ b/src/Radarr.Api.V4/Commands/CommandResource.cs @@ -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 ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Config/ConfigController.cs b/src/Radarr.Api.V4/Config/ConfigController.cs new file mode 100644 index 0000000000..ceda169932 --- /dev/null +++ b/src/Radarr.Api.V4/Config/ConfigController.cs @@ -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 : RestController + 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 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); + } +} diff --git a/src/Radarr.Api.V4/Config/DownloadClientConfigController.cs b/src/Radarr.Api.V4/Config/DownloadClientConfigController.cs new file mode 100644 index 0000000000..585025a949 --- /dev/null +++ b/src/Radarr.Api.V4/Config/DownloadClientConfigController.cs @@ -0,0 +1,19 @@ +using NzbDrone.Core.Configuration; +using Radarr.Http; + +namespace Radarr.Api.V4.Config +{ + [V4ApiController("config/downloadclient")] + public class DownloadClientConfigController : ConfigController + { + public DownloadClientConfigController(IConfigService configService) + : base(configService) + { + } + + protected override DownloadClientConfigResource ToResource(IConfigService model) + { + return DownloadClientConfigResourceMapper.ToResource(model); + } + } +} diff --git a/src/Radarr.Api.V4/Config/DownloadClientConfigResource.cs b/src/Radarr.Api.V4/Config/DownloadClientConfigResource.cs new file mode 100644 index 0000000000..84aa0339d8 --- /dev/null +++ b/src/Radarr.Api.V4/Config/DownloadClientConfigResource.cs @@ -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 + }; + } + } +} diff --git a/src/Radarr.Api.V4/Config/HostConfigController.cs b/src/Radarr.Api.V4/Config/HostConfigController.cs new file mode 100644 index 0000000000..0675a71bf5 --- /dev/null +++ b/src/Radarr.Api.V4/Config/HostConfigController.cs @@ -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 + { + 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 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); + } + } +} diff --git a/src/Radarr.Api.V4/Config/HostConfigResource.cs b/src/Radarr.Api.V4/Config/HostConfigResource.cs new file mode 100644 index 0000000000..8dd3fdfea2 --- /dev/null +++ b/src/Radarr.Api.V4/Config/HostConfigResource.cs @@ -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 + }; + } + } +} diff --git a/src/Radarr.Api.V4/Config/ImportListConfigController.cs b/src/Radarr.Api.V4/Config/ImportListConfigController.cs new file mode 100644 index 0000000000..3b6b8287d8 --- /dev/null +++ b/src/Radarr.Api.V4/Config/ImportListConfigController.cs @@ -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 + { + public ImportListConfigController(IConfigService configService) + : base(configService) + { + SharedValidator.RuleFor(c => c.ImportListSyncInterval) + .IsValidImportListSyncInterval(); + } + + protected override ImportListConfigResource ToResource(IConfigService model) + { + return ImportListConfigResourceMapper.ToResource(model); + } + } +} diff --git a/src/Radarr.Api.V4/Config/ImportListConfigResource.cs b/src/Radarr.Api.V4/Config/ImportListConfigResource.cs new file mode 100644 index 0000000000..c0bd5f9a08 --- /dev/null +++ b/src/Radarr.Api.V4/Config/ImportListConfigResource.cs @@ -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 + }; + } + } +} diff --git a/src/Radarr.Api.V4/Config/IndexerConfigController.cs b/src/Radarr.Api.V4/Config/IndexerConfigController.cs new file mode 100644 index 0000000000..0d0762e260 --- /dev/null +++ b/src/Radarr.Api.V4/Config/IndexerConfigController.cs @@ -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 + { + 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); + } + } +} diff --git a/src/Radarr.Api.V4/Config/IndexerConfigResource.cs b/src/Radarr.Api.V4/Config/IndexerConfigResource.cs new file mode 100644 index 0000000000..bba0e39f7e --- /dev/null +++ b/src/Radarr.Api.V4/Config/IndexerConfigResource.cs @@ -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, + }; + } + } +} diff --git a/src/Radarr.Api.V4/Config/MediaManagementConfigController.cs b/src/Radarr.Api.V4/Config/MediaManagementConfigController.cs new file mode 100644 index 0000000000..576399a216 --- /dev/null +++ b/src/Radarr.Api.V4/Config/MediaManagementConfigController.cs @@ -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 + { + 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); + } + } +} diff --git a/src/Radarr.Api.V4/Config/MediaManagementConfigResource.cs b/src/Radarr.Api.V4/Config/MediaManagementConfigResource.cs new file mode 100644 index 0000000000..8ac1d55ac3 --- /dev/null +++ b/src/Radarr.Api.V4/Config/MediaManagementConfigResource.cs @@ -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 + }; + } + } +} diff --git a/src/Radarr.Api.V4/Config/MetadataConfigController.cs b/src/Radarr.Api.V4/Config/MetadataConfigController.cs new file mode 100644 index 0000000000..6fc328e343 --- /dev/null +++ b/src/Radarr.Api.V4/Config/MetadataConfigController.cs @@ -0,0 +1,19 @@ +using NzbDrone.Core.Configuration; +using Radarr.Http; + +namespace Radarr.Api.V4.Config +{ + [V4ApiController("config/metadata")] + public class MetadataConfigController : ConfigController + { + public MetadataConfigController(IConfigService configService) + : base(configService) + { + } + + protected override MetadataConfigResource ToResource(IConfigService model) + { + return MetadataConfigResourceMapper.ToResource(model); + } + } +} diff --git a/src/Radarr.Api.V4/Config/MetadataConfigResource.cs b/src/Radarr.Api.V4/Config/MetadataConfigResource.cs new file mode 100644 index 0000000000..a8b33c9956 --- /dev/null +++ b/src/Radarr.Api.V4/Config/MetadataConfigResource.cs @@ -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, + }; + } + } +} diff --git a/src/Radarr.Api.V4/Config/NamingConfigController.cs b/src/Radarr.Api.V4/Config/NamingConfigController.cs new file mode 100644 index 0000000000..c9b6c47a07 --- /dev/null +++ b/src/Radarr.Api.V4/Config/NamingConfigController.cs @@ -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 + { + 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 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(); + + if (validationFailures.Any()) + { + throw new ValidationException(validationFailures.DistinctBy(v => v.PropertyName).ToArray()); + } + } + } +} diff --git a/src/Radarr.Api.V4/Config/NamingConfigResource.cs b/src/Radarr.Api.V4/Config/NamingConfigResource.cs new file mode 100644 index 0000000000..118ca757db --- /dev/null +++ b/src/Radarr.Api.V4/Config/NamingConfigResource.cs @@ -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; } + } +} diff --git a/src/Radarr.Api.V4/Config/NamingExampleResource.cs b/src/Radarr.Api.V4/Config/NamingExampleResource.cs new file mode 100644 index 0000000000..a43db04289 --- /dev/null +++ b/src/Radarr.Api.V4/Config/NamingExampleResource.cs @@ -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, + }; + } + } +} diff --git a/src/Radarr.Api.V4/Config/UiConfigController.cs b/src/Radarr.Api.V4/Config/UiConfigController.cs new file mode 100644 index 0000000000..41427aecb0 --- /dev/null +++ b/src/Radarr.Api.V4/Config/UiConfigController.cs @@ -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 + { + private readonly IConfigFileProvider _configFileProvider; + + public UiConfigController(IConfigFileProvider configFileProvider, IConfigService configService) + : base(configService) + { + _configFileProvider = configFileProvider; + } + + [RestPutById] + public override ActionResult 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); + } + } +} diff --git a/src/Radarr.Api.V4/Config/UiConfigResource.cs b/src/Radarr.Api.V4/Config/UiConfigResource.cs new file mode 100644 index 0000000000..5fe3a48f90 --- /dev/null +++ b/src/Radarr.Api.V4/Config/UiConfigResource.cs @@ -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 + }; + } + } +} diff --git a/src/Radarr.Api.V4/Credits/CreditController.cs b/src/Radarr.Api.V4/Credits/CreditController.cs new file mode 100644 index 0000000000..3b54238c1d --- /dev/null +++ b/src/Radarr.Api.V4/Credits/CreditController.cs @@ -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 + { + 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 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(); + } + } +} diff --git a/src/Radarr.Api.V4/Credits/CreditResource.cs b/src/Radarr.Api.V4/Credits/CreditResource.cs new file mode 100644 index 0000000000..c7d77e2c1b --- /dev/null +++ b/src/Radarr.Api.V4/Credits/CreditResource.cs @@ -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 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 ToResource(this IEnumerable 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 + }; + } + } +} diff --git a/src/Radarr.Api.V4/CustomFilters/CustomFilterController.cs b/src/Radarr.Api.V4/CustomFilters/CustomFilterController.cs new file mode 100644 index 0000000000..fa003eed43 --- /dev/null +++ b/src/Radarr.Api.V4/CustomFilters/CustomFilterController.cs @@ -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 + { + private readonly ICustomFilterService _customFilterService; + + public CustomFilterController(ICustomFilterService customFilterService) + { + _customFilterService = customFilterService; + } + + protected override CustomFilterResource GetResourceById(int id) + { + return _customFilterService.Get(id).ToResource(); + } + + [HttpGet] + public List GetCustomFilters() + { + return _customFilterService.All().ToResource(); + } + + [RestPostById] + public ActionResult AddCustomFilter(CustomFilterResource resource) + { + var customFilter = _customFilterService.Add(resource.ToModel()); + + return Created(customFilter.Id); + } + + [RestPutById] + public ActionResult UpdateCustomFilter(CustomFilterResource resource) + { + _customFilterService.Update(resource.ToModel()); + return Accepted(resource.Id); + } + + [RestDeleteById] + public void DeleteCustomResource(int id) + { + _customFilterService.Delete(id); + } + } +} diff --git a/src/Radarr.Api.V4/CustomFilters/CustomFilterResource.cs b/src/Radarr.Api.V4/CustomFilters/CustomFilterResource.cs new file mode 100644 index 0000000000..0f26722e0b --- /dev/null +++ b/src/Radarr.Api.V4/CustomFilters/CustomFilterResource.cs @@ -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 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>(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 ToResource(this IEnumerable filters) + { + return filters.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/CustomFormats/CustomFormatController.cs b/src/Radarr.Api.V4/CustomFormats/CustomFormatController.cs new file mode 100644 index 0000000000..967bb7ec31 --- /dev/null +++ b/src/Radarr.Api.V4/CustomFormats/CustomFormatController.cs @@ -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 + { + private readonly ICustomFormatService _formatService; + private readonly List _specifications; + + public CustomFormatController(ICustomFormatService formatService, + List 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 Create(CustomFormatResource customFormatResource) + { + var model = customFormatResource.ToModel(_specifications); + + Validate(model); + + return Created(_formatService.Insert(model).Id); + } + + [RestPutById] + [Consumes("application/json")] + public ActionResult Update(CustomFormatResource resource) + { + var model = resource.ToModel(_specifications); + + Validate(model); + + _formatService.Update(model); + + return Accepted(model.Id); + } + + [HttpGet] + [Produces("application/json")] + public List 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 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(?(\w+SUBS?)\b)|(?(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; + } + } + } + } +} diff --git a/src/Radarr.Api.V4/CustomFormats/CustomFormatResource.cs b/src/Radarr.Api.V4/CustomFormats/CustomFormatResource.cs new file mode 100644 index 0000000000..a03457d263 --- /dev/null +++ b/src/Radarr.Api.V4/CustomFormats/CustomFormatResource.cs @@ -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 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 ToResource(this IEnumerable models, bool includeDetails) + { + return models.Select(m => m.ToResource(includeDetails)).ToList(); + } + + public static CustomFormat ToModel(this CustomFormatResource resource, List 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() + }; + } + + private static ICustomFormatSpecification MapSpecification(CustomFormatSpecificationSchema resource, List 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; + } + } +} diff --git a/src/Radarr.Api.V4/CustomFormats/CustomFormatSpecificationSchema.cs b/src/Radarr.Api.V4/CustomFormats/CustomFormatSpecificationSchema.cs new file mode 100644 index 0000000000..ee371a1478 --- /dev/null +++ b/src/Radarr.Api.V4/CustomFormats/CustomFormatSpecificationSchema.cs @@ -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 Fields { get; set; } + public List 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) + }; + } + } +} diff --git a/src/Radarr.Api.V4/DiskSpace/DiskSpaceController.cs b/src/Radarr.Api.V4/DiskSpace/DiskSpaceController.cs new file mode 100644 index 0000000000..f9cab88726 --- /dev/null +++ b/src/Radarr.Api.V4/DiskSpace/DiskSpaceController.cs @@ -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 GetFreeSpace() + { + return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource); + } + } +} diff --git a/src/Radarr.Api.V4/DiskSpace/DiskSpaceResource.cs b/src/Radarr.Api.V4/DiskSpace/DiskSpaceResource.cs new file mode 100644 index 0000000000..5b5d877fe9 --- /dev/null +++ b/src/Radarr.Api.V4/DiskSpace/DiskSpaceResource.cs @@ -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 + }; + } + } +} diff --git a/src/Radarr.Api.V4/DownloadClient/DownloadClientController.cs b/src/Radarr.Api.V4/DownloadClient/DownloadClientController.cs new file mode 100644 index 0000000000..1512e13b19 --- /dev/null +++ b/src/Radarr.Api.V4/DownloadClient/DownloadClientController.cs @@ -0,0 +1,16 @@ +using NzbDrone.Core.Download; +using Radarr.Http; + +namespace Radarr.Api.V4.DownloadClient +{ + [V4ApiController] + public class DownloadClientController : ProviderControllerBase + { + public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper(); + + public DownloadClientController(IDownloadClientFactory downloadClientFactory) + : base(downloadClientFactory, "downloadclient", ResourceMapper) + { + } + } +} diff --git a/src/Radarr.Api.V4/DownloadClient/DownloadClientResource.cs b/src/Radarr.Api.V4/DownloadClient/DownloadClientResource.cs new file mode 100644 index 0000000000..c985da714b --- /dev/null +++ b/src/Radarr.Api.V4/DownloadClient/DownloadClientResource.cs @@ -0,0 +1,53 @@ +using NzbDrone.Core.Download; +using NzbDrone.Core.Indexers; + +namespace Radarr.Api.V4.DownloadClient +{ + public class DownloadClientResource : ProviderResource + { + 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 + { + 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; + } + } +} diff --git a/src/Radarr.Api.V4/ExtraFiles/ExtraFileController.cs b/src/Radarr.Api.V4/ExtraFiles/ExtraFileController.cs new file mode 100644 index 0000000000..a3ff800199 --- /dev/null +++ b/src/Radarr.Api.V4/ExtraFiles/ExtraFileController.cs @@ -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 _subtitleFileService; + private readonly IExtraFileService _metadataFileService; + private readonly IExtraFileService _otherFileService; + + public ExtraFileController(IExtraFileService subtitleFileService, IExtraFileService metadataFileService, IExtraFileService otherExtraFileService) + { + _subtitleFileService = subtitleFileService; + _metadataFileService = metadataFileService; + _otherFileService = otherExtraFileService; + } + + [HttpGet] + public List GetFiles(int movieId) + { + var extraFiles = new List(); + + List subtitleFiles = _subtitleFileService.GetFilesByMovie(movieId); + List metadataFiles = _metadataFileService.GetFilesByMovie(movieId); + List otherExtraFiles = _otherFileService.GetFilesByMovie(movieId); + + extraFiles.AddRange(subtitleFiles.ToResource()); + extraFiles.AddRange(metadataFiles.ToResource()); + extraFiles.AddRange(otherExtraFiles.ToResource()); + + return extraFiles; + } + } +} diff --git a/src/Radarr.Api.V4/ExtraFiles/ExtraFileResource.cs b/src/Radarr.Api.V4/ExtraFiles/ExtraFileResource.cs new file mode 100644 index 0000000000..b1ca1a79a2 --- /dev/null +++ b/src/Radarr.Api.V4/ExtraFiles/ExtraFileResource.cs @@ -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 ToResource(this IEnumerable movies) + { + return movies.Select(ToResource).ToList(); + } + + public static List ToResource(this IEnumerable movies) + { + return movies.Select(ToResource).ToList(); + } + + public static List ToResource(this IEnumerable movies) + { + return movies.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/FileSystem/FileSystemController.cs b/src/Radarr.Api.V4/FileSystem/FileSystemController.cs new file mode 100644 index 0000000000..2a4b2460ed --- /dev/null +++ b/src/Radarr.Api.V4/FileSystem/FileSystemController.cs @@ -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(); + } + + return _diskScanService.GetVideoFiles(path).Select(f => new + { + Path = f, + RelativePath = path.GetRelativePath(f), + Name = Path.GetFileName(f) + }); + } + } +} diff --git a/src/Radarr.Api.V4/Health/HealthController.cs b/src/Radarr.Api.V4/Health/HealthController.cs new file mode 100644 index 0000000000..d3d5d5e628 --- /dev/null +++ b/src/Radarr.Api.V4/Health/HealthController.cs @@ -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, + IHandle + { + 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 GetHealth() + { + return _healthCheckService.Results().ToResource(); + } + + [NonAction] + public void Handle(HealthCheckCompleteEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Radarr.Api.V4/Health/HealthResource.cs b/src/Radarr.Api.V4/Health/HealthResource.cs new file mode 100644 index 0000000000..b7542fae8f --- /dev/null +++ b/src/Radarr.Api.V4/Health/HealthResource.cs @@ -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 ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/History/HistoryController.cs b/src/Radarr.Api.V4/History/HistoryController.cs new file mode 100644 index 0000000000..184ba4e3dd --- /dev/null +++ b/src/Radarr.Api.V4/History/HistoryController.cs @@ -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 GetHistory(bool includeMovie) + { + var pagingResource = Request.ReadPagingResourceFromRequest(); + var pagingSpec = pagingResource.MapToPagingSpec("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 GetHistorySince(DateTime date, MovieHistoryEventType? eventType = null, bool includeMovie = false) + { + return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeMovie)).ToList(); + } + + [HttpGet("movie")] + public List 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 { }; + } + } +} diff --git a/src/Radarr.Api.V4/History/HistoryResource.cs b/src/Radarr.Api.V4/History/HistoryResource.cs new file mode 100644 index 0000000000..6b3d1a1f3e --- /dev/null +++ b/src/Radarr.Api.V4/History/HistoryResource.cs @@ -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 Languages { get; set; } + public QualityModel Quality { get; set; } + public List 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 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 + }; + } + } +} diff --git a/src/Radarr.Api.V4/ImportLists/ImportExclusionsController.cs b/src/Radarr.Api.V4/ImportLists/ImportExclusionsController.cs new file mode 100644 index 0000000000..b6cf0cf8e5 --- /dev/null +++ b/src/Radarr.Api.V4/ImportLists/ImportExclusionsController.cs @@ -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 + { + 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 GetAll() + { + return _exclusionService.GetAllExclusions().ToResource(); + } + + protected override ImportExclusionsResource GetResourceById(int id) + { + return _exclusionService.GetById(id).ToResource(); + } + + [RestPutById] + public ActionResult UpdateExclusion(ImportExclusionsResource exclusionResource) + { + var model = exclusionResource.ToModel(); + return Accepted(_exclusionService.Update(model)); + } + + [RestPostById] + public ActionResult AddExclusion(ImportExclusionsResource exclusionResource) + { + var model = exclusionResource.ToModel(); + + return Created(_exclusionService.AddExclusion(model).Id); + } + + [HttpPost("bulk")] + public object AddExclusions([FromBody] List resource) + { + var newMovies = resource.ToModel(); + + return _exclusionService.AddExclusions(newMovies).ToResource(); + } + + [RestDeleteById] + public void RemoveExclusion(int id) + { + _exclusionService.RemoveExclusion(new ImportExclusion { Id = id }); + } + } +} diff --git a/src/Radarr.Api.V4/ImportLists/ImportExclusionsResource.cs b/src/Radarr.Api.V4/ImportLists/ImportExclusionsResource.cs new file mode 100644 index 0000000000..e5144d7871 --- /dev/null +++ b/src/Radarr.Api.V4/ImportLists/ImportExclusionsResource.cs @@ -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 + { + // 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 ToResource(this IEnumerable 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 ToModel(this IEnumerable resources) + { + return resources.Select(ToModel).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/ImportLists/ImportListController.cs b/src/Radarr.Api.V4/ImportLists/ImportListController.cs new file mode 100644 index 0000000000..36c75ab38c --- /dev/null +++ b/src/Radarr.Api.V4/ImportLists/ImportListController.cs @@ -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 + { + 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); + } + } +} diff --git a/src/Radarr.Api.V4/ImportLists/ImportListMoviesController.cs b/src/Radarr.Api.V4/ImportLists/ImportListMoviesController.cs new file mode 100644 index 0000000000..c806b02476 --- /dev/null +++ b/src/Radarr.Api.V4/ImportLists/ImportListMoviesController.cs @@ -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(); + var listExclusions = _importExclusionService.GetAllExclusions(); + var existingTmdbIds = _movieService.AllMovieTmdbIds(); + + if (includeRecommendations) + { + var mapped = new List(); + + 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 resource) + { + var newMovies = resource.ToModel(); + + return _addMovieService.AddMovies(newMovies, true).ToResource(0); + } + + private IEnumerable MapToResource(IEnumerable 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 MapToResource(IEnumerable 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; + } + } + } +} diff --git a/src/Radarr.Api.V4/ImportLists/ImportListMoviesResource.cs b/src/Radarr.Api.V4/ImportLists/ImportListMoviesResource.cs new file mode 100644 index 0000000000..45df89c43e --- /dev/null +++ b/src/Radarr.Api.V4/ImportLists/ImportListMoviesResource.cs @@ -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(); + } + + 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 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 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 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 { model.ListId } + }; + } + } +} diff --git a/src/Radarr.Api.V4/ImportLists/ImportListResource.cs b/src/Radarr.Api.V4/ImportLists/ImportListResource.cs new file mode 100644 index 0000000000..8cddd7e956 --- /dev/null +++ b/src/Radarr.Api.V4/ImportLists/ImportListResource.cs @@ -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 + { + public bool Enabled { get; set; } + public bool EnableAuto { get; set; } + public MonitorTypes Monitor { get; set; } + public string RootFolderPath { get; set; } + public List 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 + { + 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; + } + } +} diff --git a/src/Radarr.Api.V4/Indexers/IndexerController.cs b/src/Radarr.Api.V4/Indexers/IndexerController.cs new file mode 100644 index 0000000000..93f8cd6cd6 --- /dev/null +++ b/src/Radarr.Api.V4/Indexers/IndexerController.cs @@ -0,0 +1,16 @@ +using NzbDrone.Core.Indexers; +using Radarr.Http; + +namespace Radarr.Api.V4.Indexers +{ + [V4ApiController] + public class IndexerController : ProviderControllerBase + { + public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); + + public IndexerController(IndexerFactory indexerFactory) + : base(indexerFactory, "indexer", ResourceMapper) + { + } + } +} diff --git a/src/Radarr.Api.V4/Indexers/IndexerFlagController.cs b/src/Radarr.Api.V4/Indexers/IndexerFlagController.cs new file mode 100644 index 0000000000..81caebb1df --- /dev/null +++ b/src/Radarr.Api.V4/Indexers/IndexerFlagController.cs @@ -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 GetAll() + { + return Enum.GetValues(typeof(IndexerFlags)).Cast().Select(f => new IndexerFlagResource + { + Id = (int)f, + Name = f.ToString() + }).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Indexers/IndexerFlagResource.cs b/src/Radarr.Api.V4/Indexers/IndexerFlagResource.cs new file mode 100644 index 0000000000..9e2e706c55 --- /dev/null +++ b/src/Radarr.Api.V4/Indexers/IndexerFlagResource.cs @@ -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(); + } +} diff --git a/src/Radarr.Api.V4/Indexers/IndexerResource.cs b/src/Radarr.Api.V4/Indexers/IndexerResource.cs new file mode 100644 index 0000000000..b28599a88f --- /dev/null +++ b/src/Radarr.Api.V4/Indexers/IndexerResource.cs @@ -0,0 +1,58 @@ +using NzbDrone.Core.Indexers; + +namespace Radarr.Api.V4.Indexers +{ + public class IndexerResource : ProviderResource + { + 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 + { + 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; + } + } +} diff --git a/src/Radarr.Api.V4/Indexers/ReleaseController.cs b/src/Radarr.Api.V4/Indexers/ReleaseController.cs new file mode 100644 index 0000000000..e3f5148266 --- /dev/null +++ b/src/Radarr.Api.V4/Indexers/ReleaseController.cs @@ -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 _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(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 GetReleases(int? movieId) + { + if (movieId.HasValue) + { + return GetMovieReleases(movieId.Value); + } + + return GetRss(); + } + + private List 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(); + } + + private List 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); + } + } +} diff --git a/src/Radarr.Api.V4/Indexers/ReleaseControllerBase.cs b/src/Radarr.Api.V4/Indexers/ReleaseControllerBase.cs new file mode 100644 index 0000000000..1c0a471908 --- /dev/null +++ b/src/Radarr.Api.V4/Indexers/ReleaseControllerBase.cs @@ -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 + { + 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 MapDecisions(IEnumerable decisions) + { + var result = new List(); + + 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; + } + } +} diff --git a/src/Radarr.Api.V4/Indexers/ReleasePushController.cs b/src/Radarr.Api.V4/Indexers/ReleasePushController.cs new file mode 100644 index 0000000000..196dc6bd75 --- /dev/null +++ b/src/Radarr.Api.V4/Indexers/ReleasePushController.cs @@ -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> 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 decisions; + + lock (PushLock) + { + decisions = _downloadDecisionMaker.GetRssDecision(new List { info }); + _downloadDecisionProcessor.ProcessDecisions(decisions); + } + + var firstDecision = decisions.FirstOrDefault(); + + if (firstDecision?.RemoteMovie.ParsedMovieInfo == null) + { + throw new ValidationException(new List { 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); + } + } + } +} diff --git a/src/Radarr.Api.V4/Indexers/ReleaseResource.cs b/src/Radarr.Api.V4/Indexers/ReleaseResource.cs new file mode 100644 index 0000000000..9ec83775dc --- /dev/null +++ b/src/Radarr.Api.V4/Indexers/ReleaseResource.cs @@ -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 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 MovieTitles { get; set; } + public List 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 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 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; + } + } +} diff --git a/src/Radarr.Api.V4/Localization/LocalizationController.cs b/src/Radarr.Api.V4/Localization/LocalizationController.cs new file mode 100644 index 0000000000..5d48e1a08b --- /dev/null +++ b/src/Radarr.Api.V4/Localization/LocalizationController.cs @@ -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); + } + } +} diff --git a/src/Radarr.Api.V4/Localization/LocalizationResource.cs b/src/Radarr.Api.V4/Localization/LocalizationResource.cs new file mode 100644 index 0000000000..2f4c28d1a5 --- /dev/null +++ b/src/Radarr.Api.V4/Localization/LocalizationResource.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Localization +{ + public class LocalizationResource : RestResource + { + public Dictionary Strings { get; set; } + } + + public static class LocalizationResourceMapper + { + public static LocalizationResource ToResource(this Dictionary localization) + { + if (localization == null) + { + return null; + } + + return new LocalizationResource + { + Strings = localization, + }; + } + } +} diff --git a/src/Radarr.Api.V4/Logs/LogController.cs b/src/Radarr.Api.V4/Logs/LogController.cs new file mode 100644 index 0000000000..0accffbddd --- /dev/null +++ b/src/Radarr.Api.V4/Logs/LogController.cs @@ -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 GetLogs() + { + var pagingResource = Request.ReadPagingResourceFromRequest(); + var pageSpec = pagingResource.MapToPagingSpec(); + + 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; + } + } +} diff --git a/src/Radarr.Api.V4/Logs/LogFileController.cs b/src/Radarr.Api.V4/Logs/LogFileController.cs new file mode 100644 index 0000000000..af93662c1e --- /dev/null +++ b/src/Radarr.Api.V4/Logs/LogFileController.cs @@ -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 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"; + } + } + } +} diff --git a/src/Radarr.Api.V4/Logs/LogFileControllerBase.cs b/src/Radarr.Api.V4/Logs/LogFileControllerBase.cs new file mode 100644 index 0000000000..3fb5a1418e --- /dev/null +++ b/src/Radarr.Api.V4/Logs/LogFileControllerBase.cs @@ -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 = @"/(?[-.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 GetLogFilesResponse() + { + var result = new List(); + + 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 GetLogFiles(); + protected abstract string GetLogFilePath(string filename); + + protected abstract string DownloadUrlRoot { get; } + } +} diff --git a/src/Radarr.Api.V4/Logs/LogFileResource.cs b/src/Radarr.Api.V4/Logs/LogFileResource.cs new file mode 100644 index 0000000000..51eaa7352f --- /dev/null +++ b/src/Radarr.Api.V4/Logs/LogFileResource.cs @@ -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; } + } +} diff --git a/src/Radarr.Api.V4/Logs/LogResource.cs b/src/Radarr.Api.V4/Logs/LogResource.cs new file mode 100644 index 0000000000..86bf03f6a1 --- /dev/null +++ b/src/Radarr.Api.V4/Logs/LogResource.cs @@ -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 + }; + } + } +} diff --git a/src/Radarr.Api.V4/Logs/UpdateLogFileController.cs b/src/Radarr.Api.V4/Logs/UpdateLogFileController.cs new file mode 100644 index 0000000000..732f9bb4db --- /dev/null +++ b/src/Radarr.Api.V4/Logs/UpdateLogFileController.cs @@ -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 GetLogFiles() + { + if (!_diskProvider.FolderExists(_appFolderInfo.GetUpdateLogFolder())) + { + return Enumerable.Empty(); + } + + 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"; + } + } + } +} diff --git a/src/Radarr.Api.V4/ManualImport/ManualImportController.cs b/src/Radarr.Api.V4/ManualImport/ManualImportController.cs new file mode 100644 index 0000000000..cb9a831e74 --- /dev/null +++ b/src/Radarr.Api.V4/ManualImport/ManualImportController.cs @@ -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 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 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; + } + } +} diff --git a/src/Radarr.Api.V4/ManualImport/ManualImportReprocessResource.cs b/src/Radarr.Api.V4/ManualImport/ManualImportReprocessResource.cs new file mode 100644 index 0000000000..27ef68a695 --- /dev/null +++ b/src/Radarr.Api.V4/ManualImport/ManualImportReprocessResource.cs @@ -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 Languages { get; set; } + public string ReleaseGroup { get; set; } + public string DownloadId { get; set; } + + public IEnumerable Rejections { get; set; } + } +} diff --git a/src/Radarr.Api.V4/ManualImport/ManualImportResource.cs b/src/Radarr.Api.V4/ManualImport/ManualImportResource.cs new file mode 100644 index 0000000000..1683f7efe3 --- /dev/null +++ b/src/Radarr.Api.V4/ManualImport/ManualImportResource.cs @@ -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 Languages { get; set; } + public string ReleaseGroup { get; set; } + public int QualityWeight { get; set; } + public string DownloadId { get; set; } + public IEnumerable 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 ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/MediaCovers/MediaCoverController.cs b/src/Radarr.Api.V4/MediaCovers/MediaCoverController.cs new file mode 100644 index 0000000000..c70d31dc06 --- /dev/null +++ b/src/Radarr.Api.V4/MediaCovers/MediaCoverController.cs @@ -0,0 +1,59 @@ +using System.IO; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using Radarr.Http; + +namespace Radarr.Api.V4.MediaCovers +{ + [V4ApiController] + public class MediaCoverController : Controller + { + private static readonly Regex RegexResizedImage = new Regex(@"-\d+\.jpg$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + private readonly IContentTypeProvider _mimeTypeProvider; + + public MediaCoverController(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) + { + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + _mimeTypeProvider = new FileExtensionContentTypeProvider(); + } + + [HttpGet(@"{movieId:int}/{filename:regex((.+)\.(jpg|png|gif))}")] + public IActionResult GetMediaCover(int movieId, string filename) + { + var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", movieId.ToString(), filename); + + if (!_diskProvider.FileExists(filePath) || _diskProvider.GetFileSize(filePath) == 0) + { + // Return the full sized image if someone requests a non-existing resized one. + // TODO: This code can be removed later once everyone had the update for a while. + var basefilePath = RegexResizedImage.Replace(filePath, ".jpg"); + if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath)) + { + return NotFound(); + } + + filePath = basefilePath; + } + + return PhysicalFile(filePath, GetContentType(filePath)); + } + + private string GetContentType(string filePath) + { + if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType)) + { + contentType = "application/octet-stream"; + } + + return contentType; + } + } +} diff --git a/src/Radarr.Api.V4/Metadata/MetadataController.cs b/src/Radarr.Api.V4/Metadata/MetadataController.cs new file mode 100644 index 0000000000..79c77a787a --- /dev/null +++ b/src/Radarr.Api.V4/Metadata/MetadataController.cs @@ -0,0 +1,16 @@ +using NzbDrone.Core.Extras.Metadata; +using Radarr.Http; + +namespace Radarr.Api.V4.Metadata +{ + [V4ApiController] + public class MetadataController : ProviderControllerBase + { + public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper(); + + public MetadataController(IMetadataFactory metadataFactory) + : base(metadataFactory, "metadata", ResourceMapper) + { + } + } +} diff --git a/src/Radarr.Api.V4/Metadata/MetadataResource.cs b/src/Radarr.Api.V4/Metadata/MetadataResource.cs new file mode 100644 index 0000000000..e1d1a02227 --- /dev/null +++ b/src/Radarr.Api.V4/Metadata/MetadataResource.cs @@ -0,0 +1,40 @@ +using NzbDrone.Core.Extras.Metadata; + +namespace Radarr.Api.V4.Metadata +{ + public class MetadataResource : ProviderResource + { + public bool Enable { get; set; } + } + + public class MetadataResourceMapper : ProviderResourceMapper + { + public override MetadataResource ToResource(MetadataDefinition definition) + { + if (definition == null) + { + return null; + } + + var resource = base.ToResource(definition); + + resource.Enable = definition.Enable; + + return resource; + } + + public override MetadataDefinition ToModel(MetadataResource resource) + { + if (resource == null) + { + return null; + } + + var definition = base.ToModel(resource); + + definition.Enable = resource.Enable; + + return definition; + } + } +} diff --git a/src/Radarr.Api.V4/MovieFiles/MediaInfoResource.cs b/src/Radarr.Api.V4/MovieFiles/MediaInfoResource.cs new file mode 100644 index 0000000000..f8f24e44c7 --- /dev/null +++ b/src/Radarr.Api.V4/MovieFiles/MediaInfoResource.cs @@ -0,0 +1,64 @@ +using System; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles.MediaInfo; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.MovieFiles +{ + public class MediaInfoResource : RestResource + { + public long AudioBitrate { get; set; } + public decimal AudioChannels { get; set; } + public string AudioCodec { get; set; } + public string AudioLanguages { get; set; } + public int AudioStreamCount { get; set; } + public int VideoBitDepth { get; set; } + public long VideoBitrate { get; set; } + public string VideoCodec { get; set; } + public string VideoDynamicRangeType { get; set; } + public decimal VideoFps { get; set; } + public string Resolution { get; set; } + public string RunTime { get; set; } + public string ScanType { get; set; } + public string Subtitles { get; set; } + } + + public static class MediaInfoResourceMapper + { + public static MediaInfoResource ToResource(this MediaInfoModel model, string sceneName) + { + if (model == null) + { + return null; + } + + return new MediaInfoResource + { + AudioBitrate = model.AudioBitrate, + AudioChannels = MediaInfoFormatter.FormatAudioChannels(model), + AudioLanguages = model.AudioLanguages.ConcatToString("/"), + AudioStreamCount = model.AudioStreamCount, + AudioCodec = MediaInfoFormatter.FormatAudioCodec(model, sceneName), + VideoBitDepth = model.VideoBitDepth, + VideoBitrate = model.VideoBitrate, + VideoCodec = MediaInfoFormatter.FormatVideoCodec(model, sceneName), + VideoDynamicRangeType = MediaInfoFormatter.FormatVideoDynamicRangeType(model), + VideoFps = Math.Round(model.VideoFps, 3), + Resolution = $"{model.Width}x{model.Height}", + RunTime = FormatRuntime(model.RunTime), + ScanType = model.ScanType, + Subtitles = model.Subtitles.ConcatToString("/") + }; + } + + private static string FormatRuntime(TimeSpan runTime) + { + if (runTime.Hours > 0) + { + return $"{runTime.Hours}:{runTime.Minutes:00}:{runTime.Seconds:00}"; + } + + return $"{runTime.Minutes}:{runTime.Seconds:00}"; + } + } +} diff --git a/src/Radarr.Api.V4/MovieFiles/MovieFileController.cs b/src/Radarr.Api.V4/MovieFiles/MovieFileController.cs new file mode 100644 index 0000000000..e711713d53 --- /dev/null +++ b/src/Radarr.Api.V4/MovieFiles/MovieFileController.cs @@ -0,0 +1,204 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.SignalR; +using Radarr.Http; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; +using BadRequestException = Radarr.Http.REST.BadRequestException; + +namespace Radarr.Api.V4.MovieFiles +{ + [V4ApiController] + public class MovieFileController : RestControllerWithSignalR, + IHandle, + IHandle + { + private readonly IMediaFileService _mediaFileService; + private readonly IDeleteMediaFiles _mediaFileDeletionService; + private readonly IMovieService _movieService; + private readonly ICustomFormatCalculationService _formatCalculator; + private readonly IUpgradableSpecification _qualityUpgradableSpecification; + + public MovieFileController(IBroadcastSignalRMessage signalRBroadcaster, + IMediaFileService mediaFileService, + IDeleteMediaFiles mediaFileDeletionService, + IMovieService movieService, + ICustomFormatCalculationService formatCalculator, + IUpgradableSpecification qualityUpgradableSpecification) + : base(signalRBroadcaster) + { + _mediaFileService = mediaFileService; + _mediaFileDeletionService = mediaFileDeletionService; + _movieService = movieService; + _formatCalculator = formatCalculator; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + } + + protected override MovieFileResource GetResourceById(int id) + { + var movieFile = _mediaFileService.GetMovie(id); + var movie = _movieService.GetMovie(movieFile.MovieId); + movieFile.Movie = movie; + + var resource = movieFile.ToResource(movie, _qualityUpgradableSpecification, _formatCalculator); + return resource; + } + + [HttpGet] + public List GetMovieFiles(int? movieId, [FromQuery] List movieFileIds) + { + if (!movieId.HasValue && !movieFileIds.Any()) + { + throw new BadRequestException("movieId or movieFileIds must be provided"); + } + + if (movieId.HasValue) + { + var movie = _movieService.GetMovie(movieId.Value); + var files = _mediaFileService.GetFilesByMovie(movieId.Value); + + if (files == null) + { + return new List(); + } + + files.ForEach(x => x.Movie = movie); + var resources = files.Select(m => m.ToResource(movie, _qualityUpgradableSpecification, _formatCalculator)).ToList(); + + return resources; + } + else + { + var movieFiles = _mediaFileService.GetMovies(movieFileIds); + + return movieFiles.GroupBy(e => e.MovieId) + .SelectMany(f => f.ToList() + .ConvertAll(e => e.ToResource(_movieService.GetMovie(f.Key), _qualityUpgradableSpecification, _formatCalculator))) + .ToList(); + } + } + + [RestPutById] + public ActionResult SetMovieFile(MovieFileResource movieFileResource) + { + var movieFile = _mediaFileService.GetMovie(movieFileResource.Id); + movieFile.IndexerFlags = (IndexerFlags)movieFileResource.IndexerFlags; + movieFile.Quality = movieFileResource.Quality; + movieFile.Languages = movieFileResource.Languages; + movieFile.Edition = movieFileResource.Edition; + if (movieFileResource.ReleaseGroup != null) + { + movieFile.ReleaseGroup = movieFileResource.ReleaseGroup; + } + + if (movieFileResource.SceneName != null && SceneChecker.IsSceneTitle(movieFileResource.SceneName)) + { + movieFile.SceneName = movieFileResource.SceneName; + } + + _mediaFileService.Update(movieFile); + return Accepted(movieFile.Id); + } + + [HttpPut("editor")] + public object SetMovieFile([FromBody] MovieFileListResource resource) + { + var movieFiles = _mediaFileService.GetMovies(resource.MovieFileIds); + + foreach (var movieFile in movieFiles) + { + if (resource.Quality != null) + { + movieFile.Quality = resource.Quality; + } + + if (resource.Languages != null) + { + // Don't allow user to set movieFile with 'Any' or 'Original' language + movieFile.Languages = resource.Languages.Where(l => l != Language.Any || l != Language.Original || l != null).ToList(); + } + + if (resource.IndexerFlags != null) + { + movieFile.IndexerFlags = (IndexerFlags)resource.IndexerFlags.Value; + } + + if (resource.Edition != null) + { + movieFile.Edition = resource.Edition; + } + + if (resource.ReleaseGroup != null) + { + movieFile.ReleaseGroup = resource.ReleaseGroup; + } + + if (resource.SceneName != null && SceneChecker.IsSceneTitle(resource.SceneName)) + { + movieFile.SceneName = resource.SceneName; + } + } + + _mediaFileService.Update(movieFiles); + + var movies = _movieService.GetMovies(movieFiles.Select(x => x.MovieId).Distinct()); + + movieFiles.ForEach(x => x.Movie = movies.SingleOrDefault(m => m.Id == x.MovieId)); + + return Accepted(movieFiles.ConvertAll(f => f.ToResource(f.Movie, _qualityUpgradableSpecification, _formatCalculator))); + } + + [RestDeleteById] + public void DeleteMovieFile(int id) + { + var movieFile = _mediaFileService.GetMovie(id); + + if (movieFile == null) + { + throw new NzbDroneClientException(global::System.Net.HttpStatusCode.NotFound, "Movie file not found"); + } + + var movie = _movieService.GetMovie(movieFile.MovieId); + + _mediaFileDeletionService.DeleteMovieFile(movie, movieFile); + } + + [HttpDelete("bulk")] + public object DeleteMovieFiles([FromBody] MovieFileListResource resource) + { + var movieFiles = _mediaFileService.GetMovies(resource.MovieFileIds); + var movie = _movieService.GetMovie(movieFiles.First().MovieId); + + foreach (var movieFile in movieFiles) + { + _mediaFileDeletionService.DeleteMovieFile(movie, movieFile); + } + + return new { }; + } + + [NonAction] + public void Handle(MovieFileAddedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.MovieFile.Id); + } + + [NonAction] + public void Handle(MovieFileDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, message.MovieFile.Id); + } + } +} diff --git a/src/Radarr.Api.V4/MovieFiles/MovieFileListResource.cs b/src/Radarr.Api.V4/MovieFiles/MovieFileListResource.cs new file mode 100644 index 0000000000..48ea74f493 --- /dev/null +++ b/src/Radarr.Api.V4/MovieFiles/MovieFileListResource.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; + +namespace Radarr.Api.V4.MovieFiles +{ + public class MovieFileListResource + { + public List MovieFileIds { get; set; } + public List Languages { get; set; } + public QualityModel Quality { get; set; } + public string Edition { get; set; } + public string ReleaseGroup { get; set; } + public string SceneName { get; set; } + public int? IndexerFlags { get; set; } + } +} diff --git a/src/Radarr.Api.V4/MovieFiles/MovieFileResource.cs b/src/Radarr.Api.V4/MovieFiles/MovieFileResource.cs new file mode 100644 index 0000000000..d606ea24da --- /dev/null +++ b/src/Radarr.Api.V4/MovieFiles/MovieFileResource.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.IO; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using Radarr.Api.V4.CustomFormats; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.MovieFiles +{ + public class MovieFileResource : RestResource + { + public int MovieId { get; set; } + public string RelativePath { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + public string SceneName { get; set; } + public int IndexerFlags { get; set; } + public QualityModel Quality { get; set; } + public List CustomFormats { get; set; } + public MediaInfoResource MediaInfo { get; set; } + public string OriginalFilePath { get; set; } + public bool QualityCutoffNotMet { get; set; } + public List Languages { get; set; } + public string ReleaseGroup { get; set; } + public string Edition { get; set; } + } + + public static class MovieFileResourceMapper + { + private static MovieFileResource ToResource(this MovieFile model) + { + if (model == null) + { + return null; + } + + return new MovieFileResource + { + Id = model.Id, + + MovieId = model.MovieId, + RelativePath = model.RelativePath, + + // Path + Size = model.Size, + DateAdded = model.DateAdded, + SceneName = model.SceneName, + IndexerFlags = (int)model.IndexerFlags, + Quality = model.Quality, + Languages = model.Languages, + ReleaseGroup = model.ReleaseGroup, + Edition = model.Edition, + MediaInfo = model.MediaInfo.ToResource(model.SceneName), + OriginalFilePath = model.OriginalFilePath + }; + } + + public static MovieFileResource ToResource(this MovieFile model, NzbDrone.Core.Movies.Movie movie, IUpgradableSpecification upgradableSpecification, ICustomFormatCalculationService formatCalculator) + { + if (model == null) + { + return null; + } + + return new MovieFileResource + { + Id = model.Id, + + MovieId = model.MovieId, + RelativePath = model.RelativePath, + Path = Path.Combine(movie.Path, model.RelativePath), + Size = model.Size, + DateAdded = model.DateAdded, + SceneName = model.SceneName, + IndexerFlags = (int)model.IndexerFlags, + Quality = model.Quality, + Languages = model.Languages, + Edition = model.Edition, + ReleaseGroup = model.ReleaseGroup, + MediaInfo = model.MediaInfo.ToResource(model.SceneName), + QualityCutoffNotMet = upgradableSpecification?.QualityCutoffNotMet(movie.Profile, model.Quality) ?? false, + OriginalFilePath = model.OriginalFilePath, + CustomFormats = formatCalculator.ParseCustomFormat(model).ToResource(false) + }; + } + } +} diff --git a/src/Radarr.Api.V4/Movies/AlternativeTitleController.cs b/src/Radarr.Api.V4/Movies/AlternativeTitleController.cs new file mode 100644 index 0000000000..4173e5aade --- /dev/null +++ b/src/Radarr.Api.V4/Movies/AlternativeTitleController.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.AlternativeTitles; +using Radarr.Http; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Movies +{ + [V4ApiController("alttitle")] + public class AlternativeTitleController : RestController + { + private readonly IAlternativeTitleService _altTitleService; + private readonly IMovieService _movieService; + + public AlternativeTitleController(IAlternativeTitleService altTitleService, IMovieService movieService) + { + _altTitleService = altTitleService; + _movieService = movieService; + } + + protected override AlternativeTitleResource GetResourceById(int id) + { + return _altTitleService.GetById(id).ToResource(); + } + + [HttpGet] + public List GetAltTitles(int? movieId, int? movieMetadataId) + { + if (movieMetadataId.HasValue) + { + return _altTitleService.GetAllTitlesForMovieMetadata(movieMetadataId.Value).ToResource(); + } + + if (movieId.HasValue) + { + var movie = _movieService.GetMovie(movieId.Value); + return _altTitleService.GetAllTitlesForMovieMetadata(movie.MovieMetadataId).ToResource(); + } + + return _altTitleService.GetAllTitles().ToResource(); + } + } +} diff --git a/src/Radarr.Api.V4/Movies/AlternativeTitleResource.cs b/src/Radarr.Api.V4/Movies/AlternativeTitleResource.cs new file mode 100644 index 0000000000..a2e25d95fd --- /dev/null +++ b/src/Radarr.Api.V4/Movies/AlternativeTitleResource.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Movies.AlternativeTitles; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Movies +{ + public class AlternativeTitleResource : RestResource + { + public AlternativeTitleResource() + { + } + + // Todo: Sorters should be done completely on the client + // Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + // Todo: We should get the entire Profile instead of ID and Name separately + public int MovieMetadataId { get; set; } + public string Title { get; set; } + public string CleanTitle { get; set; } + public int SourceId { get; set; } + public int Votes { get; set; } + public int VoteCount { get; set; } + public Language Language { get; set; } + + // TODO: Add series statistics as a property of the series (instead of individual properties) + } + + public static class AlternativeTitleResourceMapper + { + public static AlternativeTitleResource ToResource(this AlternativeTitle model) + { + if (model == null) + { + return null; + } + + return new AlternativeTitleResource + { + Id = model.Id, + SourceType = model.SourceType, + MovieMetadataId = model.MovieMetadataId, + Title = model.Title, + SourceId = model.SourceId, + Votes = model.Votes, + VoteCount = model.VoteCount, + Language = model.Language + }; + } + + public static AlternativeTitle ToModel(this AlternativeTitleResource resource) + { + if (resource == null) + { + return null; + } + + return new AlternativeTitle + { + Id = resource.Id, + SourceType = resource.SourceType, + MovieMetadataId = resource.MovieMetadataId, + Title = resource.Title, + SourceId = resource.SourceId, + Votes = resource.Votes, + VoteCount = resource.VoteCount, + Language = resource.Language + }; + } + + public static List ToResource(this IEnumerable movies) + { + return movies.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Movies/MovieController.cs b/src/Radarr.Api.V4/Movies/MovieController.cs new file mode 100644 index 0000000000..7a1a777e09 --- /dev/null +++ b/src/Radarr.Api.V4/Movies/MovieController.cs @@ -0,0 +1,364 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Commands; +using NzbDrone.Core.Movies.Events; +using NzbDrone.Core.Movies.Translations; +using NzbDrone.Core.MovieStats; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; +using Radarr.Http; +using Radarr.Http.Extensions; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V4.Movies +{ + [V4ApiController] + public class MovieController : RestControllerWithSignalR, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle + { + private readonly IMovieService _moviesService; + private readonly IMovieStatisticsService _movieStatisticsService; + private readonly IMovieTranslationService _movieTranslationService; + private readonly IAddMovieService _addMovieService; + private readonly IMapCoversToLocal _coverMapper; + private readonly IManageCommandQueue _commandQueueManager; + private readonly IUpgradableSpecification _qualityUpgradableSpecification; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public MovieController(IBroadcastSignalRMessage signalRBroadcaster, + IMovieService moviesService, + IMovieStatisticsService movieStatisticsService, + IMovieTranslationService movieTranslationService, + IAddMovieService addMovieService, + IMapCoversToLocal coverMapper, + IManageCommandQueue commandQueueManager, + IUpgradableSpecification qualityUpgradableSpecification, + IConfigService configService, + RootFolderValidator rootFolderValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator, + MoviePathValidator moviesPathValidator, + MovieExistsValidator moviesExistsValidator, + MovieAncestorValidator moviesAncestorValidator, + RecycleBinValidator recycleBinValidator, + SystemFolderValidator systemFolderValidator, + ProfileExistsValidator profileExistsValidator, + MovieFolderAsRootFolderValidator movieFolderAsRootFolderValidator, + Logger logger) + : base(signalRBroadcaster) + { + _moviesService = moviesService; + _movieStatisticsService = movieStatisticsService; + _movieTranslationService = movieTranslationService; + _addMovieService = addMovieService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + _configService = configService; + _coverMapper = coverMapper; + _commandQueueManager = commandQueueManager; + _logger = logger; + + SharedValidator.RuleFor(s => s.QualityProfileId).ValidId().When(s => s.QualityProfileIds == null || s.QualityProfileIds.Empty()); + + SharedValidator.RuleFor(s => s.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(moviesPathValidator) + .SetValidator(moviesAncestorValidator) + .SetValidator(recycleBinValidator) + .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() + .SetValidator(movieFolderAsRootFolderValidator) + .When(s => s.Path.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.Title).NotEmpty().When(s => s.TmdbId <= 0); + PostValidator.RuleFor(s => s.TmdbId).NotNull().NotEmpty().SetValidator(moviesExistsValidator); + + PutValidator.RuleFor(s => s.Path).IsValidPath(); + } + + [HttpGet] + public List AllMovie(int? tmdbId) + { + var moviesResources = new List(); + + Dictionary coverFileInfos = null; + + if (tmdbId.HasValue) + { + var movie = _moviesService.FindByTmdbId(tmdbId.Value); + + if (movie != null) + { + moviesResources.AddIfNotNull(MapToResource(movie)); + } + } + else + { + var configLanguage = (Language)_configService.MovieInfoLanguage; + var availDelay = _configService.AvailabilityDelay; + var movieStats = _movieStatisticsService.MovieStatistics(); + + var movieTask = Task.Run(() => _moviesService.GetAllMovies()); + + var translations = _movieTranslationService + .GetAllTranslationsForLanguage(configLanguage); + + var tdict = translations.ToDictionary(x => x.MovieMetadataId); + + coverFileInfos = _coverMapper.GetCoverFileInfos(); + + var movies = movieTask.GetAwaiter().GetResult(); + + moviesResources = new List(movies.Count); + + foreach (var movie in movies) + { + var translation = GetTranslationFromDict(tdict, movie.MovieMetadata, configLanguage); + moviesResources.Add(movie.ToResource(availDelay, translation, _qualityUpgradableSpecification)); + } + + MapCoversToLocal(moviesResources, coverFileInfos); + LinkMovieStatistics(moviesResources, movieStats); + } + + return moviesResources; + } + + protected override MovieResource GetResourceById(int id) + { + var movie = _moviesService.GetMovie(id); + + return MapToResource(movie); + } + + protected MovieResource MapToResource(Movie movie) + { + if (movie == null) + { + return null; + } + + var availDelay = _configService.AvailabilityDelay; + + var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.MovieMetadataId); + var translation = GetMovieTranslation(translations, movie.MovieMetadata, (Language)_configService.MovieInfoLanguage); + + var resource = movie.ToResource(availDelay, translation, _qualityUpgradableSpecification); + MapCoversToLocal(resource); + FetchAndLinkMovieStatistics(resource); + + return resource; + } + + private MovieTranslation GetMovieTranslation(List translations, MovieMetadata movie, Language configLanguage) + { + if (configLanguage == Language.Original) + { + return new MovieTranslation + { + Title = movie.OriginalTitle, + Overview = movie.Overview + }; + } + + return translations.FirstOrDefault(t => t.Language == configLanguage && t.MovieMetadataId == movie.Id); + } + + private MovieTranslation GetTranslationFromDict(Dictionary translations, MovieMetadata movie, Language configLanguage) + { + if (configLanguage == Language.Original) + { + return new MovieTranslation + { + Title = movie.OriginalTitle, + Overview = movie.Overview + }; + } + + translations.TryGetValue(movie.Id, out var translation); + return translation; + } + + private void FetchAndLinkMovieStatistics(MovieResource resource) + { + LinkMovieStatistics(resource, _movieStatisticsService.MovieStatistics(resource.Id)); + } + + private void LinkMovieStatistics(List resources, List seriesStatistics) + { + foreach (var series in resources) + { + var stats = seriesStatistics.SingleOrDefault(ss => ss.MovieId == series.Id); + if (stats == null) + { + continue; + } + + LinkMovieStatistics(series, stats); + } + } + + private void LinkMovieStatistics(MovieResource resource, MovieStatistics seriesStatistics) + { + resource.Statistics = seriesStatistics.ToResource(); + } + + [RestPostById] + public ActionResult AddMovie(MovieResource moviesResource) + { + var movie = _addMovieService.AddMovie(moviesResource.ToModel()); + + return Created(movie.Id); + } + + [RestPutById] + public ActionResult UpdateMovie(MovieResource moviesResource) + { + var moveFiles = Request.GetBooleanQueryParameter("moveFiles"); + var movie = _moviesService.GetMovie(moviesResource.Id); + + if (moveFiles) + { + var sourcePath = movie.Path; + var destinationPath = moviesResource.Path; + + _commandQueueManager.Push(new MoveMovieCommand + { + MovieId = movie.Id, + SourcePath = sourcePath, + DestinationPath = destinationPath, + Trigger = CommandTrigger.Manual + }); + } + + var model = moviesResource.ToModel(movie); + + var updatedMovie = _moviesService.UpdateMovie(model); + var availDelay = _configService.AvailabilityDelay; + + var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.MovieMetadataId); + var translation = GetMovieTranslation(translations, movie.MovieMetadata, (Language)_configService.MovieInfoLanguage); + + BroadcastResourceChange(ModelAction.Updated, updatedMovie.ToResource(availDelay, translation, _qualityUpgradableSpecification)); + + return Accepted(moviesResource.Id); + } + + [RestDeleteById] + public void DeleteMovie(int id) + { + var addExclusion = Request.GetBooleanQueryParameter("addImportExclusion"); + var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); + + _moviesService.DeleteMovie(id, deleteFiles, addExclusion); + } + + private void MapCoversToLocal(MovieResource movie) + { + _coverMapper.ConvertToLocalUrls(movie.Id, movie.Images); + } + + private void MapCoversToLocal(IEnumerable movies, Dictionary coverFileInfos) + { + _coverMapper.ConvertToLocalUrls(movies.Select(x => Tuple.Create(x.Id, x.Images.AsEnumerable())), coverFileInfos); + } + + [NonAction] + public void Handle(MovieFileImportedEvent message) + { + var availDelay = _configService.AvailabilityDelay; + var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(message.ImportedMovie.Movie.MovieMetadataId); + var translation = GetMovieTranslation(translations, message.ImportedMovie.Movie.MovieMetadata, (Language)_configService.MovieInfoLanguage); + BroadcastResourceChange(ModelAction.Updated, message.ImportedMovie.Movie.ToResource(availDelay, translation, _qualityUpgradableSpecification)); + } + + [NonAction] + public void Handle(MovieFileDeletedEvent message) + { + if (message.Reason == DeleteMediaFileReason.Upgrade) + { + return; + } + + BroadcastResourceChange(ModelAction.Updated, message.MovieFile.MovieId); + } + + [NonAction] + public void Handle(MovieUpdatedEvent message) + { + var availDelay = _configService.AvailabilityDelay; + var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(message.Movie.MovieMetadataId); + var translation = GetMovieTranslation(translations, message.Movie.MovieMetadata, (Language)_configService.MovieInfoLanguage); + BroadcastResourceChange(ModelAction.Updated, message.Movie.ToResource(availDelay, translation, _qualityUpgradableSpecification)); + } + + [NonAction] + public void Handle(MovieEditedEvent message) + { + var availDelay = _configService.AvailabilityDelay; + var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(message.Movie.MovieMetadataId); + var translation = GetMovieTranslation(translations, message.Movie.MovieMetadata, (Language)_configService.MovieInfoLanguage); + BroadcastResourceChange(ModelAction.Updated, message.Movie.ToResource(availDelay, translation, _qualityUpgradableSpecification)); + } + + [NonAction] + public void Handle(MoviesDeletedEvent message) + { + foreach (var movie in message.Movies) + { + BroadcastResourceChange(ModelAction.Deleted, movie.Id); + } + } + + [NonAction] + public void Handle(MovieRenamedEvent message) + { + var availDelay = _configService.AvailabilityDelay; + var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(message.Movie.MovieMetadataId); + var translation = GetMovieTranslation(translations, message.Movie.MovieMetadata, (Language)_configService.MovieInfoLanguage); + BroadcastResourceChange(ModelAction.Updated, message.Movie.ToResource(availDelay, translation, _qualityUpgradableSpecification)); + } + + [NonAction] + public void Handle(MediaCoversUpdatedEvent message) + { + if (message.Updated) + { + BroadcastResourceChange(ModelAction.Updated, message.Movie.Id); + } + } + } +} diff --git a/src/Radarr.Api.V4/Movies/MovieEditorController.cs b/src/Radarr.Api.V4/Movies/MovieEditorController.cs new file mode 100644 index 0000000000..7ec8d82ffb --- /dev/null +++ b/src/Radarr.Api.V4/Movies/MovieEditorController.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Commands; +using Radarr.Http; + +namespace Radarr.Api.V4.Movies +{ + [V4ApiController("movie/editor")] + public class MovieEditorController : Controller + { + private readonly IMovieService _movieService; + private readonly IManageCommandQueue _commandQueueManager; + private readonly IUpgradableSpecification _upgradableSpecification; + + public MovieEditorController(IMovieService movieService, IManageCommandQueue commandQueueManager, IUpgradableSpecification upgradableSpecification) + { + _movieService = movieService; + _commandQueueManager = commandQueueManager; + _upgradableSpecification = upgradableSpecification; + } + + [HttpPut] + public IActionResult SaveAll([FromBody] MovieEditorResource resource) + { + var moviesToUpdate = _movieService.GetMovies(resource.MovieIds); + var moviesToMove = new List(); + + foreach (var movie in moviesToUpdate) + { + if (resource.Monitored.HasValue) + { + movie.Monitored = resource.Monitored.Value; + } + + if (resource.QualityProfileIds != null) + { + movie.QualityProfileIds = resource.QualityProfileIds; + } + + if (resource.MinimumAvailability.HasValue) + { + movie.MinimumAvailability = resource.MinimumAvailability.Value; + } + + if (resource.RootFolderPath.IsNotNullOrWhiteSpace()) + { + movie.RootFolderPath = resource.RootFolderPath; + moviesToMove.Add(new BulkMoveMovie + { + MovieId = movie.Id, + SourcePath = movie.Path + }); + } + + if (resource.Tags != null) + { + var newTags = resource.Tags; + var applyTags = resource.ApplyTags; + + switch (applyTags) + { + case ApplyTags.Add: + newTags.ForEach(t => movie.Tags.Add(t)); + break; + case ApplyTags.Remove: + newTags.ForEach(t => movie.Tags.Remove(t)); + break; + case ApplyTags.Replace: + movie.Tags = new HashSet(newTags); + break; + } + } + } + + if (resource.MoveFiles && moviesToMove.Any()) + { + _commandQueueManager.Push(new BulkMoveMovieCommand + { + DestinationRootFolder = resource.RootFolderPath, + Movies = moviesToMove + }); + } + + return Accepted(_movieService.UpdateMovie(moviesToUpdate, !resource.MoveFiles).ToResource(0, _upgradableSpecification)); + } + + [HttpDelete] + public object DeleteMovies([FromBody] MovieEditorResource resource) + { + _movieService.DeleteMovies(resource.MovieIds, resource.DeleteFiles, resource.AddImportExclusion); + + return new { }; + } + } +} diff --git a/src/Radarr.Api.V4/Movies/MovieEditorResource.cs b/src/Radarr.Api.V4/Movies/MovieEditorResource.cs new file mode 100644 index 0000000000..79b7452d2b --- /dev/null +++ b/src/Radarr.Api.V4/Movies/MovieEditorResource.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using NzbDrone.Core.Movies; + +namespace Radarr.Api.V4.Movies +{ + public class MovieEditorResource + { + public List MovieIds { get; set; } + public bool? Monitored { get; set; } + public List QualityProfileIds { get; set; } + public MovieStatusType? MinimumAvailability { get; set; } + public string RootFolderPath { get; set; } + public List Tags { get; set; } + public ApplyTags ApplyTags { get; set; } + public bool MoveFiles { get; set; } + public bool DeleteFiles { get; set; } + public bool AddImportExclusion { get; set; } + } + + public enum ApplyTags + { + Add, + Remove, + Replace + } +} diff --git a/src/Radarr.Api.V4/Movies/MovieFolderAsRootFolderValidator.cs b/src/Radarr.Api.V4/Movies/MovieFolderAsRootFolderValidator.cs new file mode 100644 index 0000000000..ce6807178a --- /dev/null +++ b/src/Radarr.Api.V4/Movies/MovieFolderAsRootFolderValidator.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Organizer; + +namespace Radarr.Api.V4.Movies +{ + public class MovieFolderAsRootFolderValidator : PropertyValidator + { + private readonly IBuildFileNames _fileNameBuilder; + + public MovieFolderAsRootFolderValidator(IBuildFileNames fileNameBuilder) + : base("Root folder path contains movie folder") + { + _fileNameBuilder = fileNameBuilder; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) + { + return true; + } + + var movieResource = context.Instance as MovieResource; + + if (movieResource == null) + { + return true; + } + + var rootFolderPath = context.PropertyValue.ToString(); + + if (rootFolderPath.IsNullOrWhiteSpace()) + { + return true; + } + + var rootFolder = new DirectoryInfo(rootFolderPath).Name; + var movie = movieResource.ToModel(); + var movieFolder = _fileNameBuilder.GetMovieFolder(movie); + + if (movieFolder == rootFolder) + { + return false; + } + + var distance = movieFolder.LevenshteinDistance(rootFolder); + + return distance >= Math.Max(1, movieFolder.Length * 0.2); + } + } +} diff --git a/src/Radarr.Api.V4/Movies/MovieImportController.cs b/src/Radarr.Api.V4/Movies/MovieImportController.cs new file mode 100644 index 0000000000..1061348d46 --- /dev/null +++ b/src/Radarr.Api.V4/Movies/MovieImportController.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Movies; +using Radarr.Http; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Movies +{ + [V4ApiController("movie/import")] + public class MovieImportController : RestController + { + private readonly IAddMovieService _addMovieService; + + public MovieImportController(IAddMovieService addMovieService) + { + _addMovieService = addMovieService; + } + + protected override MovieResource GetResourceById(int id) + { + throw new NotImplementedException(); + } + + [HttpPost] + public object Import([FromBody] List resource) + { + var newMovies = resource.ToModel(); + + return _addMovieService.AddMovies(newMovies).ToResource(0); + } + } +} diff --git a/src/Radarr.Api.V4/Movies/MovieLookupController.cs b/src/Radarr.Api.V4/Movies/MovieLookupController.cs new file mode 100644 index 0000000000..4031e31ce0 --- /dev/null +++ b/src/Radarr.Api.V4/Movies/MovieLookupController.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Organizer; +using Radarr.Http; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Movies +{ + [V4ApiController("movie/lookup")] + public class MovieLookupController : RestController + { + private readonly ISearchForNewMovie _searchProxy; + private readonly IProvideMovieInfo _movieInfo; + private readonly IBuildFileNames _fileNameBuilder; + private readonly IMapCoversToLocal _coverMapper; + private readonly IConfigService _configService; + + public MovieLookupController(ISearchForNewMovie searchProxy, + IProvideMovieInfo movieInfo, + IBuildFileNames fileNameBuilder, + IMapCoversToLocal coverMapper, + IConfigService configService) + { + _movieInfo = movieInfo; + _searchProxy = searchProxy; + _fileNameBuilder = fileNameBuilder; + _coverMapper = coverMapper; + _configService = configService; + } + + protected override MovieResource GetResourceById(int id) + { + throw new NotImplementedException(); + } + + [HttpGet("tmdb")] + public object SearchByTmdbId(int tmdbId) + { + var availDelay = _configService.AvailabilityDelay; + var result = new Movie { MovieMetadata = _movieInfo.GetMovieInfo(tmdbId).Item1 }; + var translation = result.MovieMetadata.Value.Translations.FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage); + return result.ToResource(availDelay, translation); + } + + [HttpGet("imdb")] + public object SearchByImdbId(string imdbId) + { + var result = new Movie { MovieMetadata = _movieInfo.GetMovieByImdbId(imdbId) }; + + var availDelay = _configService.AvailabilityDelay; + var translation = result.MovieMetadata.Value.Translations.FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage); + return result.ToResource(availDelay, translation); + } + + [HttpGet] + public object Search([FromQuery] string term) + { + var searchResults = _searchProxy.SearchForNewMovie(term); + + return MapToResource(searchResults); + } + + private IEnumerable MapToResource(IEnumerable movies) + { + foreach (var currentMovie in movies) + { + var availDelay = _configService.AvailabilityDelay; + var translation = currentMovie.MovieMetadata.Value.Translations.FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage); + var resource = currentMovie.ToResource(availDelay, translation); + + _coverMapper.ConvertToLocalUrls(resource.Id, resource.Images); + + var poster = currentMovie.MovieMetadata.Value.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + if (poster != null) + { + resource.RemotePoster = poster.RemoteUrl; + } + + resource.Folder = _fileNameBuilder.GetMovieFolder(currentMovie); + + yield return resource; + } + } + } +} diff --git a/src/Radarr.Api.V4/Movies/MovieResource.cs b/src/Radarr.Api.V4/Movies/MovieResource.cs new file mode 100644 index 0000000000..29d774dbe3 --- /dev/null +++ b/src/Radarr.Api.V4/Movies/MovieResource.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; +using NzbDrone.Core.Movies.Translations; +using NzbDrone.Core.Parser; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Movies +{ + public class MovieResource : RestResource + { + public MovieResource() + { + Monitored = true; + MinimumAvailability = MovieStatusType.Released; + QualityProfileIds = new List(); + } + + // Todo: Sorters should be done completely on the client + // Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + // Todo: We should get the entire Profile instead of ID and Name separately + + // View Only + public string Title { get; set; } + public string OriginalTitle { get; set; } + public Language OriginalLanguage { get; set; } + public List AlternateTitles { get; set; } + public int? SecondaryYear { get; set; } + public int SecondaryYearSourceId { 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 string PhysicalReleaseNote { get; set; } + public List Images { get; set; } + public string Website { get; set; } + + // public bool Downloaded { get; set; } + public string RemotePoster { get; set; } + public int Year { get; set; } + public string YouTubeTrailerId { get; set; } + public string Studio { get; set; } + + // View & Edit + public string Path { get; set; } + public List QualityProfileIds { get; set; } + + // Compatabilitiy + public int QualityProfileId { get; set; } + + // Editing Only + public bool Monitored { get; set; } + public MovieStatusType MinimumAvailability { get; set; } + public bool IsAvailable { get; set; } + public string FolderName { get; set; } + + public int Runtime { get; set; } + public string CleanTitle { get; set; } + public string ImdbId { get; set; } + public int TmdbId { get; set; } + public string TitleSlug { get; set; } + public string RootFolderPath { get; set; } + public string Folder { get; set; } + public string Certification { get; set; } + public List Genres { get; set; } + public HashSet Tags { get; set; } + public DateTime Added { get; set; } + public AddMovieOptions AddOptions { get; set; } + public Ratings Ratings { get; set; } + public MovieCollection Collection { get; set; } + public float Popularity { get; set; } + public MovieStatisticsResource Statistics { get; set; } + } + + public static class MovieResourceMapper + { + public static MovieResource ToResource(this Movie model, int availDelay, MovieTranslation movieTranslation = null, IUpgradableSpecification upgradableSpecification = null) + { + if (model == null) + { + return null; + } + + var translatedTitle = movieTranslation?.Title ?? model.Title; + var translatedOverview = movieTranslation?.Overview ?? model.MovieMetadata.Value.Overview; + + var collection = model.MovieMetadata.Value.CollectionTmdbId > 0 ? new MovieCollection { Title = model.MovieMetadata.Value.CollectionTitle, TmdbId = model.MovieMetadata.Value.CollectionTmdbId } : null; + + return new MovieResource + { + Id = model.Id, + TmdbId = model.TmdbId, + Title = translatedTitle, + OriginalTitle = model.MovieMetadata.Value.OriginalTitle, + OriginalLanguage = model.MovieMetadata.Value.OriginalLanguage, + SortTitle = translatedTitle.NormalizeTitle(), + InCinemas = model.MovieMetadata.Value.InCinemas, + PhysicalRelease = model.MovieMetadata.Value.PhysicalRelease, + DigitalRelease = model.MovieMetadata.Value.DigitalRelease, + + Status = model.MovieMetadata.Value.Status, + Overview = translatedOverview, + + Images = model.MovieMetadata.Value.Images, + + Year = model.Year, + SecondaryYear = model.MovieMetadata.Value.SecondaryYear, + + Path = model.Path, + QualityProfileIds = model.QualityProfileIds, + QualityProfileId = model.QualityProfileIds.FirstOrDefault(), + + Monitored = model.Monitored, + MinimumAvailability = model.MinimumAvailability, + + IsAvailable = model.IsAvailable(availDelay), + FolderName = model.FolderName(), + + Runtime = model.MovieMetadata.Value.Runtime, + CleanTitle = model.MovieMetadata.Value.CleanTitle, + ImdbId = model.ImdbId, + TitleSlug = model.MovieMetadata.Value.TmdbId.ToString(), + RootFolderPath = model.RootFolderPath, + Certification = model.MovieMetadata.Value.Certification, + Website = model.MovieMetadata.Value.Website, + Genres = model.MovieMetadata.Value.Genres, + Tags = model.Tags, + Added = model.Added, + AddOptions = model.AddOptions, + AlternateTitles = model.MovieMetadata.Value.AlternativeTitles.ToResource(), + Ratings = model.MovieMetadata.Value.Ratings, + YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId, + Studio = model.MovieMetadata.Value.Studio, + Collection = collection, + Popularity = model.MovieMetadata.Value.Popularity + }; + } + + public static Movie ToModel(this MovieResource resource) + { + if (resource == null) + { + return null; + } + + var profiles = resource.QualityProfileIds; + + if (resource.QualityProfileIds.Count == 0) + { + profiles.Add(resource.QualityProfileId); + } + + return new Movie + { + Id = resource.Id, + + MovieMetadata = new MovieMetadata + { + TmdbId = resource.TmdbId, + Title = resource.Title, + Genres = resource.Genres, + Images = resource.Images, + OriginalTitle = resource.OriginalTitle, + SortTitle = resource.SortTitle, + InCinemas = resource.InCinemas, + PhysicalRelease = resource.PhysicalRelease, + Year = resource.Year, + SecondaryYear = resource.SecondaryYear, + Overview = resource.Overview, + Certification = resource.Certification, + Website = resource.Website, + Ratings = resource.Ratings, + YouTubeTrailerId = resource.YouTubeTrailerId, + Studio = resource.Studio, + Runtime = resource.Runtime, + CleanTitle = resource.CleanTitle, + ImdbId = resource.ImdbId, + }, + + Path = resource.Path, + QualityProfileIds = resource.QualityProfileIds, + + Monitored = resource.Monitored, + MinimumAvailability = resource.MinimumAvailability, + + RootFolderPath = resource.RootFolderPath, + + Tags = resource.Tags, + Added = resource.Added, + AddOptions = resource.AddOptions + }; + } + + public static Movie ToModel(this MovieResource resource, Movie movie) + { + var updatedmovie = resource.ToModel(); + + movie.ApplyChanges(updatedmovie); + + return movie; + } + + public static List ToResource(this IEnumerable movies, int availDelay, IUpgradableSpecification upgradableSpecification = null) + { + return movies.Select(x => ToResource(x, availDelay, null, upgradableSpecification)).ToList(); + } + + public static List ToModel(this IEnumerable resources) + { + return resources.Select(ToModel).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Movies/MovieStatisticsResource.cs b/src/Radarr.Api.V4/Movies/MovieStatisticsResource.cs new file mode 100644 index 0000000000..4df4045b7f --- /dev/null +++ b/src/Radarr.Api.V4/Movies/MovieStatisticsResource.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using NzbDrone.Core.MovieStats; + +namespace Radarr.Api.V4.Movies +{ + public class MovieStatisticsResource + { + public int MovieFileCount { get; set; } + public long SizeOnDisk { get; set; } + public List ReleaseGroups { get; set; } + } + + public static class SeriesStatisticsResourceMapper + { + public static MovieStatisticsResource ToResource(this MovieStatistics model) + { + if (model == null) + { + return null; + } + + return new MovieStatisticsResource + { + MovieFileCount = model.MovieFileCount, + SizeOnDisk = model.SizeOnDisk, + ReleaseGroups = model.ReleaseGroups + }; + } + } +} diff --git a/src/Radarr.Api.V4/Movies/RenameMovieController.cs b/src/Radarr.Api.V4/Movies/RenameMovieController.cs new file mode 100644 index 0000000000..9165b6fa99 --- /dev/null +++ b/src/Radarr.Api.V4/Movies/RenameMovieController.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.MediaFiles; +using Radarr.Http; + +namespace Radarr.Api.V4.Movies +{ + [V4ApiController("rename")] + public class RenameMovieController : Controller + { + private readonly IRenameMovieFileService _renameMovieFileService; + + public RenameMovieController(IRenameMovieFileService renameMovieFileService) + { + _renameMovieFileService = renameMovieFileService; + } + + [HttpGet] + public List GetMovies(int movieId) + { + return _renameMovieFileService.GetRenamePreviews(movieId).ToResource(); + } + } +} diff --git a/src/Radarr.Api.V4/Movies/RenameMovieResource.cs b/src/Radarr.Api.V4/Movies/RenameMovieResource.cs new file mode 100644 index 0000000000..933450b293 --- /dev/null +++ b/src/Radarr.Api.V4/Movies/RenameMovieResource.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.MediaFiles; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Movies +{ + public class RenameMovieResource : RestResource + { + public int MovieId { get; set; } + public int MovieFileId { get; set; } + public string ExistingPath { get; set; } + public string NewPath { get; set; } + } + + public static class RenameMovieResourceMapper + { + public static RenameMovieResource ToResource(this RenameMovieFilePreview model) + { + if (model == null) + { + return null; + } + + return new RenameMovieResource + { + MovieId = model.MovieId, + MovieFileId = model.MovieFileId, + ExistingPath = model.ExistingPath, + NewPath = model.NewPath + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Notifications/NotificationController.cs b/src/Radarr.Api.V4/Notifications/NotificationController.cs new file mode 100644 index 0000000000..b883344652 --- /dev/null +++ b/src/Radarr.Api.V4/Notifications/NotificationController.cs @@ -0,0 +1,16 @@ +using NzbDrone.Core.Notifications; +using Radarr.Http; + +namespace Radarr.Api.V4.Notifications +{ + [V4ApiController] + public class NotificationController : ProviderControllerBase + { + public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper(); + + public NotificationController(NotificationFactory notificationFactory) + : base(notificationFactory, "notification", ResourceMapper) + { + } + } +} diff --git a/src/Radarr.Api.V4/Notifications/NotificationResource.cs b/src/Radarr.Api.V4/Notifications/NotificationResource.cs new file mode 100644 index 0000000000..cb7e38e29f --- /dev/null +++ b/src/Radarr.Api.V4/Notifications/NotificationResource.cs @@ -0,0 +1,102 @@ +using NzbDrone.Core.Notifications; + +namespace Radarr.Api.V4.Notifications +{ + public class NotificationResource : ProviderResource + { + public string Link { get; set; } + public bool OnGrab { get; set; } + public bool OnDownload { get; set; } + public bool OnUpgrade { get; set; } + public bool OnRename { get; set; } + public bool OnMovieAdded { get; set; } + public bool OnMovieDelete { get; set; } + public bool OnMovieFileDelete { get; set; } + public bool OnMovieFileDeleteForUpgrade { get; set; } + public bool OnHealthIssue { get; set; } + public bool OnApplicationUpdate { get; set; } + public bool SupportsOnGrab { get; set; } + public bool SupportsOnDownload { get; set; } + public bool SupportsOnUpgrade { get; set; } + public bool SupportsOnRename { get; set; } + public bool SupportsOnMovieAdded { get; set; } + public bool SupportsOnMovieDelete { get; set; } + public bool SupportsOnMovieFileDelete { get; set; } + public bool SupportsOnMovieFileDeleteForUpgrade { get; set; } + public bool SupportsOnHealthIssue { get; set; } + public bool SupportsOnApplicationUpdate { get; set; } + public bool IncludeHealthWarnings { get; set; } + public string TestCommand { get; set; } + } + + public class NotificationResourceMapper : ProviderResourceMapper + { + public override NotificationResource ToResource(NotificationDefinition definition) + { + if (definition == null) + { + return default(NotificationResource); + } + + var resource = base.ToResource(definition); + + resource.OnGrab = definition.OnGrab; + resource.OnDownload = definition.OnDownload; + resource.OnUpgrade = definition.OnUpgrade; + resource.OnRename = definition.OnRename; + resource.OnMovieAdded = definition.OnMovieAdded; + resource.OnMovieDelete = definition.OnMovieDelete; + resource.OnMovieFileDelete = definition.OnMovieFileDelete; + resource.OnMovieFileDeleteForUpgrade = definition.OnMovieFileDeleteForUpgrade; + resource.OnHealthIssue = definition.OnHealthIssue; + resource.OnApplicationUpdate = definition.OnApplicationUpdate; + resource.SupportsOnGrab = definition.SupportsOnGrab; + resource.SupportsOnDownload = definition.SupportsOnDownload; + resource.SupportsOnUpgrade = definition.SupportsOnUpgrade; + resource.SupportsOnRename = definition.SupportsOnRename; + resource.SupportsOnMovieAdded = definition.SupportsOnMovieAdded; + resource.SupportsOnMovieDelete = definition.SupportsOnMovieDelete; + resource.SupportsOnMovieFileDelete = definition.SupportsOnMovieFileDelete; + resource.SupportsOnMovieFileDeleteForUpgrade = definition.SupportsOnMovieFileDeleteForUpgrade; + resource.SupportsOnHealthIssue = definition.SupportsOnHealthIssue; + resource.IncludeHealthWarnings = definition.IncludeHealthWarnings; + resource.SupportsOnApplicationUpdate = definition.SupportsOnApplicationUpdate; + + return resource; + } + + public override NotificationDefinition ToModel(NotificationResource resource) + { + if (resource == null) + { + return default(NotificationDefinition); + } + + var definition = base.ToModel(resource); + + definition.OnGrab = resource.OnGrab; + definition.OnDownload = resource.OnDownload; + definition.OnUpgrade = resource.OnUpgrade; + definition.OnRename = resource.OnRename; + definition.OnMovieAdded = resource.OnMovieAdded; + definition.OnMovieDelete = resource.OnMovieDelete; + definition.OnMovieFileDelete = resource.OnMovieFileDelete; + definition.OnMovieFileDeleteForUpgrade = resource.OnMovieFileDeleteForUpgrade; + definition.OnHealthIssue = resource.OnHealthIssue; + definition.OnApplicationUpdate = resource.OnApplicationUpdate; + definition.SupportsOnGrab = resource.SupportsOnGrab; + definition.SupportsOnDownload = resource.SupportsOnDownload; + definition.SupportsOnUpgrade = resource.SupportsOnUpgrade; + definition.SupportsOnRename = resource.SupportsOnRename; + definition.SupportsOnMovieAdded = resource.SupportsOnMovieAdded; + definition.SupportsOnMovieDelete = resource.SupportsOnMovieDelete; + definition.SupportsOnMovieFileDelete = resource.SupportsOnMovieFileDelete; + definition.SupportsOnMovieFileDeleteForUpgrade = resource.SupportsOnMovieFileDeleteForUpgrade; + definition.SupportsOnHealthIssue = resource.SupportsOnHealthIssue; + definition.IncludeHealthWarnings = resource.IncludeHealthWarnings; + definition.SupportsOnApplicationUpdate = resource.SupportsOnApplicationUpdate; + + return definition; + } + } +} diff --git a/src/Radarr.Api.V4/Parse/ParseController.cs b/src/Radarr.Api.V4/Parse/ParseController.cs new file mode 100644 index 0000000000..bde378cbc4 --- /dev/null +++ b/src/Radarr.Api.V4/Parse/ParseController.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; +using Radarr.Api.V4.Movies; +using Radarr.Http; + +namespace Radarr.Api.V4.Parse +{ + [V4ApiController] + public class ParseController : Controller + { + private readonly IParsingService _parsingService; + private readonly IConfigService _configService; + + public ParseController(IParsingService parsingService, IConfigService configService) + { + _parsingService = parsingService; + _configService = configService; + } + + [HttpGet] + public ParseResource Parse(string title) + { + if (title.IsNullOrWhiteSpace()) + { + return null; + } + + var parsedMovieInfo = _parsingService.ParseMovieInfo(title, new List()); + + if (parsedMovieInfo == null) + { + return new ParseResource + { + Title = title + }; + } + + var remoteMovie = _parsingService.Map(parsedMovieInfo, ""); + + if (remoteMovie != null) + { + return new ParseResource + { + Title = title, + ParsedMovieInfo = remoteMovie.RemoteMovie.ParsedMovieInfo, + Movie = remoteMovie.Movie.ToResource(_configService.AvailabilityDelay) + }; + } + else + { + return new ParseResource + { + Title = title, + ParsedMovieInfo = parsedMovieInfo + }; + } + } + } +} diff --git a/src/Radarr.Api.V4/Parse/ParseResource.cs b/src/Radarr.Api.V4/Parse/ParseResource.cs new file mode 100644 index 0000000000..a9af69bc55 --- /dev/null +++ b/src/Radarr.Api.V4/Parse/ParseResource.cs @@ -0,0 +1,13 @@ +using NzbDrone.Core.Parser.Model; +using Radarr.Api.V4.Movies; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Parse +{ + public class ParseResource : RestResource + { + public string Title { get; set; } + public ParsedMovieInfo ParsedMovieInfo { get; set; } + public MovieResource Movie { get; set; } + } +} diff --git a/src/Radarr.Api.V4/Profiles/Delay/DelayProfileController.cs b/src/Radarr.Api.V4/Profiles/Delay/DelayProfileController.cs new file mode 100644 index 0000000000..fc0645b6c4 --- /dev/null +++ b/src/Radarr.Api.V4/Profiles/Delay/DelayProfileController.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Profiles.Delay; +using Radarr.Http; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; +using Radarr.Http.Validation; + +namespace Radarr.Api.V4.Profiles.Delay +{ + [V4ApiController] + public class DelayProfileController : RestController + { + private readonly IDelayProfileService _delayProfileService; + + public DelayProfileController(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator) + { + _delayProfileService = delayProfileService; + + SharedValidator.RuleFor(d => d.Tags).NotEmpty().When(d => d.Id != 1); + SharedValidator.RuleFor(d => d.Tags).EmptyCollection().When(d => d.Id == 1); + SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator); + SharedValidator.RuleFor(d => d.UsenetDelay).GreaterThanOrEqualTo(0); + SharedValidator.RuleFor(d => d.TorrentDelay).GreaterThanOrEqualTo(0); + + SharedValidator.RuleFor(d => d).Custom((delayProfile, context) => + { + if (!delayProfile.EnableUsenet && !delayProfile.EnableTorrent) + { + context.AddFailure("Either Usenet or Torrent should be enabled"); + } + }); + } + + [RestPostById] + public ActionResult Create(DelayProfileResource resource) + { + var model = resource.ToModel(); + model = _delayProfileService.Add(model); + + return Created(model.Id); + } + + [RestDeleteById] + public void DeleteProfile(int id) + { + if (id == 1) + { + throw new MethodNotAllowedException("Cannot delete global delay profile"); + } + + _delayProfileService.Delete(id); + } + + [RestPutById] + public ActionResult Update(DelayProfileResource resource) + { + var model = resource.ToModel(); + _delayProfileService.Update(model); + return Accepted(model.Id); + } + + protected override DelayProfileResource GetResourceById(int id) + { + return _delayProfileService.Get(id).ToResource(); + } + + [HttpGet] + public List GetAll() + { + return _delayProfileService.All().ToResource(); + } + } +} diff --git a/src/Radarr.Api.V4/Profiles/Delay/DelayProfileResource.cs b/src/Radarr.Api.V4/Profiles/Delay/DelayProfileResource.cs new file mode 100644 index 0000000000..f1da8fda89 --- /dev/null +++ b/src/Radarr.Api.V4/Profiles/Delay/DelayProfileResource.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Profiles.Delay; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Profiles.Delay +{ + public class DelayProfileResource : RestResource + { + public bool EnableUsenet { get; set; } + public bool EnableTorrent { get; set; } + public DownloadProtocol PreferredProtocol { get; set; } + public int UsenetDelay { get; set; } + public int TorrentDelay { get; set; } + public bool BypassIfHighestQuality { get; set; } + public int Order { get; set; } + public HashSet Tags { get; set; } + } + + public static class DelayProfileResourceMapper + { + public static DelayProfileResource ToResource(this DelayProfile model) + { + if (model == null) + { + return null; + } + + return new DelayProfileResource + { + Id = model.Id, + + EnableUsenet = model.EnableUsenet, + EnableTorrent = model.EnableTorrent, + PreferredProtocol = model.PreferredProtocol, + UsenetDelay = model.UsenetDelay, + TorrentDelay = model.TorrentDelay, + BypassIfHighestQuality = model.BypassIfHighestQuality, + Order = model.Order, + Tags = new HashSet(model.Tags) + }; + } + + public static DelayProfile ToModel(this DelayProfileResource resource) + { + if (resource == null) + { + return null; + } + + return new DelayProfile + { + Id = resource.Id, + + EnableUsenet = resource.EnableUsenet, + EnableTorrent = resource.EnableTorrent, + PreferredProtocol = resource.PreferredProtocol, + UsenetDelay = resource.UsenetDelay, + TorrentDelay = resource.TorrentDelay, + BypassIfHighestQuality = resource.BypassIfHighestQuality, + Order = resource.Order, + Tags = new HashSet(resource.Tags) + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Profiles/Languages/LanguageController.cs b/src/Radarr.Api.V4/Profiles/Languages/LanguageController.cs new file mode 100644 index 0000000000..43040c5433 --- /dev/null +++ b/src/Radarr.Api.V4/Profiles/Languages/LanguageController.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Languages; +using Radarr.Http; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Profiles.Languages +{ + [V4ApiController] + public class LanguageController : RestController + { + protected override LanguageResource GetResourceById(int id) + { + var language = (Language)id; + + return new LanguageResource + { + Id = (int)language, + Name = language.ToString() + }; + } + + [HttpGet] + public List GetAll() + { + var languageResources = Language.All.Select(l => new LanguageResource + { + Id = (int)l, + Name = l.ToString() + }) + .OrderBy(l => l.Id > 0).ThenBy(l => l.Name) + .ToList(); + + return languageResources; + } + } +} diff --git a/src/Radarr.Api.V4/Profiles/Languages/LanguageResource.cs b/src/Radarr.Api.V4/Profiles/Languages/LanguageResource.cs new file mode 100644 index 0000000000..b8ff601012 --- /dev/null +++ b/src/Radarr.Api.V4/Profiles/Languages/LanguageResource.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Profiles.Languages +{ + public class LanguageResource : RestResource + { + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public new int Id { get; set; } + public string Name { get; set; } + public string NameLower => Name.ToLowerInvariant(); + } +} diff --git a/src/Radarr.Api.V4/Profiles/Quality/QualityCutoffValidator.cs b/src/Radarr.Api.V4/Profiles/Quality/QualityCutoffValidator.cs new file mode 100644 index 0000000000..dab9367c86 --- /dev/null +++ b/src/Radarr.Api.V4/Profiles/Quality/QualityCutoffValidator.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; + +namespace Radarr.Api.V4.Profiles.Quality +{ + public static class QualityCutoffValidator + { + public static IRuleBuilderOptions ValidCutoff(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetValidator(new ValidCutoffValidator()); + } + } + + public class ValidCutoffValidator : PropertyValidator + { + public ValidCutoffValidator() + : base("Cutoff must be an allowed quality or group") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var cutoff = (int)context.PropertyValue; + dynamic instance = context.ParentContext.InstanceToValidate; + var items = instance.Items as IList; + + var cutoffItem = items.SingleOrDefault(i => (i.Quality == null && i.Id == cutoff) || i.Quality?.Id == cutoff); + + if (cutoffItem == null) + { + return false; + } + + if (!cutoffItem.Allowed) + { + return false; + } + + return true; + } + } +} diff --git a/src/Radarr.Api.V4/Profiles/Quality/QualityItemsValidator.cs b/src/Radarr.Api.V4/Profiles/Quality/QualityItemsValidator.cs new file mode 100644 index 0000000000..2bb5c5ab23 --- /dev/null +++ b/src/Radarr.Api.V4/Profiles/Quality/QualityItemsValidator.cs @@ -0,0 +1,189 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; + +namespace Radarr.Api.V4.Profiles.Quality +{ + public static class QualityItemsValidator + { + public static IRuleBuilderOptions> ValidItems(this IRuleBuilder> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + ruleBuilder.SetValidator(new AllowedValidator()); + ruleBuilder.SetValidator(new QualityNameValidator()); + ruleBuilder.SetValidator(new GroupItemValidator()); + ruleBuilder.SetValidator(new ItemGroupIdValidator()); + ruleBuilder.SetValidator(new UniqueIdValidator()); + ruleBuilder.SetValidator(new UniqueQualityIdValidator()); + return ruleBuilder.SetValidator(new ItemGroupNameValidator()); + } + } + + public class AllowedValidator : PropertyValidator + { + public AllowedValidator() + : base("Must contain at least one allowed quality") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var list = context.PropertyValue as IList; + + if (list == null) + { + return false; + } + + if (!list.Any(c => c.Allowed)) + { + return false; + } + + return true; + } + } + + public class GroupItemValidator : PropertyValidator + { + public GroupItemValidator() + : base("Groups must contain multiple qualities") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Count <= 1)) + { + return false; + } + + return true; + } + } + + public class QualityNameValidator : PropertyValidator + { + public QualityNameValidator() + : base("Individual qualities should not be named") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Quality != null)) + { + return false; + } + + return true; + } + } + + public class ItemGroupNameValidator : PropertyValidator + { + public ItemGroupNameValidator() + : base("Groups must have a name") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Quality == null && i.Name.IsNullOrWhiteSpace())) + { + return false; + } + + return true; + } + } + + public class ItemGroupIdValidator : PropertyValidator + { + public ItemGroupIdValidator() + : base("Groups must have an ID") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Quality == null && i.Id == 0)) + { + return false; + } + + return true; + } + } + + public class UniqueIdValidator : PropertyValidator + { + public UniqueIdValidator() + : base("Groups must have a unique ID") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Where(i => i.Id > 0).Select(i => i.Id).GroupBy(i => i).Any(g => g.Count() > 1)) + { + return false; + } + + return true; + } + } + + public class UniqueQualityIdValidator : PropertyValidator + { + public UniqueQualityIdValidator() + : base("Qualities can only be used once") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + var qualityIds = new HashSet(); + + foreach (var item in items) + { + if (item.Id > 0) + { + foreach (var quality in item.Items) + { + if (qualityIds.Contains(quality.Quality.Id)) + { + return false; + } + + qualityIds.Add(quality.Quality.Id); + } + } + else + { + if (qualityIds.Contains(item.Quality.Id)) + { + return false; + } + + qualityIds.Add(item.Quality.Id); + } + } + + return true; + } + } +} diff --git a/src/Radarr.Api.V4/Profiles/Quality/QualityProfileController.cs b/src/Radarr.Api.V4/Profiles/Quality/QualityProfileController.cs new file mode 100644 index 0000000000..cec2dd8f0e --- /dev/null +++ b/src/Radarr.Api.V4/Profiles/Quality/QualityProfileController.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Profiles; +using Radarr.Http; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V4.Profiles.Quality +{ + [V4ApiController] + public class QualityProfileController : RestController + { + private readonly IProfileService _profileService; + private readonly ICustomFormatService _formatService; + + public QualityProfileController(IProfileService profileService, ICustomFormatService formatService) + { + _profileService = profileService; + _formatService = formatService; + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + + // TODO: Need to validate the cutoff is allowed and the ID/quality ID exists + // TODO: Need to validate the Items to ensure groups have names and at no item has no name, no items and no quality + SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff(); + SharedValidator.RuleFor(c => c.Items).ValidItems(); + SharedValidator.RuleFor(c => c.FormatItems).Must(items => + { + var all = _formatService.All().Select(f => f.Id).ToList(); + var ids = items.Select(i => i.Format); + + return all.Except(ids).Empty(); + }).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser."); + SharedValidator.RuleFor(c => c).Custom((profile, context) => + { + if (profile.FormatItems.Where(x => x.Score > 0).Sum(x => x.Score) < profile.MinFormatScore && + profile.FormatItems.Max(x => x.Score) < profile.MinFormatScore) + { + context.AddFailure("Minimum Custom Format Score can never be satisfied"); + } + }); + } + + [RestPostById] + public ActionResult Create(QualityProfileResource resource) + { + var model = resource.ToModel(); + model = _profileService.Add(model); + return Created(model.Id); + } + + [RestDeleteById] + public void DeleteProfile(int id) + { + _profileService.Delete(id); + } + + [RestPutById] + public ActionResult Update(QualityProfileResource resource) + { + var model = resource.ToModel(); + + _profileService.Update(model); + + return Accepted(model.Id); + } + + protected override QualityProfileResource GetResourceById(int id) + { + return _profileService.Get(id).ToResource(); + } + + [HttpGet] + public List GetAll() + { + return _profileService.All().ToResource(); + } + } +} diff --git a/src/Radarr.Api.V4/Profiles/Quality/QualityProfileResource.cs b/src/Radarr.Api.V4/Profiles/Quality/QualityProfileResource.cs new file mode 100644 index 0000000000..b51f2fa267 --- /dev/null +++ b/src/Radarr.Api.V4/Profiles/Quality/QualityProfileResource.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Profiles.Quality +{ + public class QualityProfileResource : RestResource + { + public string Name { get; set; } + public bool UpgradeAllowed { get; set; } + public int Cutoff { get; set; } + public List Items { get; set; } + public int MinFormatScore { get; set; } + public int CutoffFormatScore { get; set; } + public List FormatItems { get; set; } + public Language Language { get; set; } + } + + public class QualityProfileQualityItemResource : RestResource + { + public QualityProfileQualityItemResource() + { + Items = new List(); + } + + public string Name { get; set; } + public NzbDrone.Core.Qualities.Quality Quality { get; set; } + public List Items { get; set; } + public bool Allowed { get; set; } + } + + public class ProfileFormatItemResource : RestResource + { + public int Format { get; set; } + public string Name { get; set; } + public int Score { get; set; } + } + + public static class ProfileResourceMapper + { + public static QualityProfileResource ToResource(this Profile model) + { + if (model == null) + { + return null; + } + + return new QualityProfileResource + { + Id = model.Id, + Name = model.Name, + UpgradeAllowed = model.UpgradeAllowed, + Cutoff = model.Cutoff, + Items = model.Items.ConvertAll(ToResource), + MinFormatScore = model.MinFormatScore, + CutoffFormatScore = model.CutoffFormatScore, + FormatItems = model.FormatItems.ConvertAll(ToResource), + Language = model.Language + }; + } + + public static QualityProfileQualityItemResource ToResource(this ProfileQualityItem model) + { + if (model == null) + { + return null; + } + + return new QualityProfileQualityItemResource + { + Id = model.Id, + Name = model.Name, + Quality = model.Quality, + Items = model.Items.ConvertAll(ToResource), + Allowed = model.Allowed + }; + } + + public static ProfileFormatItemResource ToResource(this ProfileFormatItem model) + { + return new ProfileFormatItemResource + { + Format = model.Format.Id, + Name = model.Format.Name, + Score = model.Score + }; + } + + public static Profile ToModel(this QualityProfileResource resource) + { + if (resource == null) + { + return null; + } + + return new Profile + { + Id = resource.Id, + Name = resource.Name, + UpgradeAllowed = resource.UpgradeAllowed, + Cutoff = resource.Cutoff, + Items = resource.Items.ConvertAll(ToModel), + MinFormatScore = resource.MinFormatScore, + CutoffFormatScore = resource.CutoffFormatScore, + FormatItems = resource.FormatItems.ConvertAll(ToModel), + Language = resource.Language + }; + } + + public static ProfileQualityItem ToModel(this QualityProfileQualityItemResource resource) + { + if (resource == null) + { + return null; + } + + return new ProfileQualityItem + { + Id = resource.Id, + Name = resource.Name, + Quality = resource.Quality != null ? (NzbDrone.Core.Qualities.Quality)resource.Quality.Id : null, + Items = resource.Items.ConvertAll(ToModel), + Allowed = resource.Allowed + }; + } + + public static ProfileFormatItem ToModel(this ProfileFormatItemResource resource) + { + return new ProfileFormatItem + { + Format = new CustomFormat { Id = resource.Format }, + Score = resource.Score + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Profiles/Quality/QualityProfileSchemaController.cs b/src/Radarr.Api.V4/Profiles/Quality/QualityProfileSchemaController.cs new file mode 100644 index 0000000000..ae26b217d1 --- /dev/null +++ b/src/Radarr.Api.V4/Profiles/Quality/QualityProfileSchemaController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Profiles; +using Radarr.Http; + +namespace Radarr.Api.V4.Profiles.Quality +{ + [V4ApiController("qualityprofile/schema")] + public class QualityProfileSchemaController : Controller + { + private readonly IProfileService _profileService; + + public QualityProfileSchemaController(IProfileService profileService) + { + _profileService = profileService; + } + + [HttpGet] + public QualityProfileResource GetSchema() + { + var qualityProfile = _profileService.GetDefaultProfile(string.Empty); + + return qualityProfile.ToResource(); + } + } +} diff --git a/src/Radarr.Api.V4/ProviderControllerBase.cs b/src/Radarr.Api.V4/ProviderControllerBase.cs new file mode 100644 index 0000000000..9360da8d0c --- /dev/null +++ b/src/Radarr.Api.V4/ProviderControllerBase.cs @@ -0,0 +1,209 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; +using Radarr.Http.Extensions; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V4 +{ + public abstract class ProviderControllerBase : RestController + where TProviderDefinition : ProviderDefinition, new() + where TProvider : IProvider + where TProviderResource : ProviderResource, new() + { + private readonly IProviderFactory _providerFactory; + private readonly ProviderResourceMapper _resourceMapper; + + protected ProviderControllerBase(IProviderFactory providerFactory, string resource, ProviderResourceMapper resourceMapper) + { + _providerFactory = providerFactory; + _resourceMapper = resourceMapper; + + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); + SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); + SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty(); + + PostValidator.RuleFor(c => c.Fields).NotNull(); + } + + protected override TProviderResource GetResourceById(int id) + { + var definition = _providerFactory.Get(id); + _providerFactory.SetProviderCharacteristics(definition); + + return _resourceMapper.ToResource(definition); + } + + [HttpGet] + public List GetAll() + { + var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName); + + var result = new List(providerDefinitions.Count()); + + foreach (var definition in providerDefinitions) + { + _providerFactory.SetProviderCharacteristics(definition); + + result.Add(_resourceMapper.ToResource(definition)); + } + + return result.OrderBy(p => p.Name).ToList(); + } + + [RestPostById] + public ActionResult CreateProvider(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, true, false, false); + + if (providerDefinition.Enable) + { + Test(providerDefinition, false); + } + + providerDefinition = _providerFactory.Create(providerDefinition); + + return Created(providerDefinition.Id); + } + + [RestPutById] + public ActionResult UpdateProvider(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, true, false, false); + var forceSave = Request.GetBooleanQueryParameter("forceSave"); + + // Only test existing definitions if it is enabled and forceSave isn't set. + if (providerDefinition.Enable && !forceSave) + { + Test(providerDefinition, false); + } + + _providerFactory.Update(providerDefinition); + + return Accepted(providerResource.Id); + } + + private TProviderDefinition GetDefinition(TProviderResource providerResource, bool validate, bool includeWarnings, bool forceValidate) + { + var definition = _resourceMapper.ToModel(providerResource); + + if (validate && (definition.Enable || forceValidate)) + { + Validate(definition, includeWarnings); + } + + return definition; + } + + [RestDeleteById] + public object DeleteProvider(int id) + { + _providerFactory.Delete(id); + return new { }; + } + + [HttpGet("schema")] + public List GetTemplates() + { + var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList(); + + var result = new List(defaultDefinitions.Count); + + foreach (var providerDefinition in defaultDefinitions) + { + var providerResource = _resourceMapper.ToResource(providerDefinition); + var presetDefinitions = _providerFactory.GetPresetDefinitions(providerDefinition); + + providerResource.Presets = presetDefinitions + .Select(v => _resourceMapper.ToResource(v)) + .ToList(); + + result.Add(providerResource); + } + + return result; + } + + [SkipValidation(true, false)] + [HttpPost("test")] + public object Test([FromBody] TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, true, true, true); + + Test(providerDefinition, true); + + return "{}"; + } + + [HttpPost("testall")] + public IActionResult TestAll() + { + var providerDefinitions = _providerFactory.All() + .Where(c => c.Settings.Validate().IsValid && c.Enable) + .ToList(); + var result = new List(); + + foreach (var definition in providerDefinitions) + { + var validationResult = _providerFactory.Test(definition); + + result.Add(new ProviderTestAllResult + { + Id = definition.Id, + ValidationFailures = validationResult.Errors.ToList() + }); + } + + return result.Any(c => !c.IsValid) ? BadRequest(result) : Ok(result); + } + + [SkipValidation] + [HttpPost("action/{name}")] + public IActionResult RequestAction(string name, [FromBody] TProviderResource resource) + { + var providerDefinition = GetDefinition(resource, false, false, false); + + var query = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString()); + + var data = _providerFactory.RequestAction(providerDefinition, name, query); + + return Content(data.ToJson(), "application/json"); + } + + private void Validate(TProviderDefinition definition, bool includeWarnings) + { + var validationResult = definition.Settings.Validate(); + + VerifyValidationResult(validationResult, includeWarnings); + } + + protected virtual void Test(TProviderDefinition definition, bool includeWarnings) + { + var validationResult = _providerFactory.Test(definition); + + VerifyValidationResult(validationResult, includeWarnings); + } + + protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings) + { + var result = new NzbDroneValidationResult(validationResult.Errors); + + if (includeWarnings && (!result.IsValid || result.HasWarnings)) + { + throw new ValidationException(result.Failures); + } + + if (!result.IsValid) + { + throw new ValidationException(result.Errors); + } + } + } +} diff --git a/src/Radarr.Api.V4/ProviderResource.cs b/src/Radarr.Api.V4/ProviderResource.cs new file mode 100644 index 0000000000..f8b6d59cd4 --- /dev/null +++ b/src/Radarr.Api.V4/ProviderResource.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using NzbDrone.Common.Reflection; +using NzbDrone.Core.ThingiProvider; +using Radarr.Http.ClientSchema; +using Radarr.Http.REST; + +namespace Radarr.Api.V4 +{ + public class ProviderResource : RestResource + { + public string Name { get; set; } + public List Fields { get; set; } + public string ImplementationName { get; set; } + public string Implementation { get; set; } + public string ConfigContract { get; set; } + public string InfoLink { get; set; } + public ProviderMessage Message { get; set; } + public HashSet Tags { get; set; } + + public List Presets { get; set; } + } + + public class ProviderResourceMapper + where TProviderResource : ProviderResource, new() + where TProviderDefinition : ProviderDefinition, new() + { + public virtual TProviderResource ToResource(TProviderDefinition definition) + { + return new TProviderResource + { + Id = definition.Id, + + Name = definition.Name, + ImplementationName = definition.ImplementationName, + Implementation = definition.Implementation, + ConfigContract = definition.ConfigContract, + Message = definition.Message, + Tags = definition.Tags, + Fields = SchemaBuilder.ToSchema(definition.Settings), + + // radarr/supported is an disambagation page. the # should be a header on the page with appropiate details/link + InfoLink = string.Format("https://wiki.servarr.com/radarr/supported#{0}", + definition.Implementation.ToLower()) + }; + } + + public virtual TProviderDefinition ToModel(TProviderResource resource) + { + if (resource == null) + { + return default(TProviderDefinition); + } + + var definition = new TProviderDefinition + { + Id = resource.Id, + + Name = resource.Name, + ImplementationName = resource.ImplementationName, + Implementation = resource.Implementation, + ConfigContract = resource.ConfigContract, + Message = resource.Message, + Tags = resource.Tags + }; + + var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract); + definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract); + + return definition; + } + } +} diff --git a/src/Radarr.Api.V4/ProviderTestAllResult.cs b/src/Radarr.Api.V4/ProviderTestAllResult.cs new file mode 100644 index 0000000000..771fda3af9 --- /dev/null +++ b/src/Radarr.Api.V4/ProviderTestAllResult.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; + +namespace Radarr.Api.V4 +{ + public class ProviderTestAllResult + { + public int Id { get; set; } + public bool IsValid => ValidationFailures.Empty(); + public List ValidationFailures { get; set; } + + public ProviderTestAllResult() + { + ValidationFailures = new List(); + } + } +} diff --git a/src/Radarr.Api.V4/Qualities/QualityDefinitionController.cs b/src/Radarr.Api.V4/Qualities/QualityDefinitionController.cs new file mode 100644 index 0000000000..4f8fdbec9d --- /dev/null +++ b/src/Radarr.Api.V4/Qualities/QualityDefinitionController.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Qualities; +using Radarr.Http; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V4.Qualities +{ + [V4ApiController] + public class QualityDefinitionController : RestController + { + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityDefinitionController(IQualityDefinitionService qualityDefinitionService) + { + _qualityDefinitionService = qualityDefinitionService; + } + + [RestPutById] + public ActionResult Update(QualityDefinitionResource resource) + { + var model = resource.ToModel(); + _qualityDefinitionService.Update(model); + return Accepted(model.Id); + } + + protected override QualityDefinitionResource GetResourceById(int id) + { + return _qualityDefinitionService.GetById(id).ToResource(); + } + + [HttpGet] + public List GetAll() + { + return _qualityDefinitionService.All().ToResource(); + } + + [HttpPut("update")] + public object UpdateMany([FromBody] List resource) + { + // Read from request + var qualityDefinitions = resource + .ToModel() + .ToList(); + + _qualityDefinitionService.UpdateMany(qualityDefinitions); + + return Accepted(_qualityDefinitionService.All() + .ToResource()); + } + } +} diff --git a/src/Radarr.Api.V4/Qualities/QualityDefinitionResource.cs b/src/Radarr.Api.V4/Qualities/QualityDefinitionResource.cs new file mode 100644 index 0000000000..0e55315bf7 --- /dev/null +++ b/src/Radarr.Api.V4/Qualities/QualityDefinitionResource.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Qualities; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Qualities +{ + public class QualityDefinitionResource : RestResource + { + public Quality Quality { get; set; } + + public string Title { get; set; } + + public int Weight { get; set; } + + public double? MinSize { get; set; } + public double? MaxSize { get; set; } + public double? PreferredSize { get; set; } + } + + public static class QualityDefinitionResourceMapper + { + public static QualityDefinitionResource ToResource(this QualityDefinition model) + { + if (model == null) + { + return null; + } + + return new QualityDefinitionResource + { + Id = model.Id, + + Quality = model.Quality, + + Title = model.Title, + + Weight = model.Weight, + + MinSize = model.MinSize, + MaxSize = model.MaxSize, + PreferredSize = model.PreferredSize + }; + } + + public static QualityDefinition ToModel(this QualityDefinitionResource resource) + { + if (resource == null) + { + return null; + } + + return new QualityDefinition + { + Id = resource.Id, + + Quality = resource.Quality, + + Title = resource.Title, + + Weight = resource.Weight, + + MinSize = resource.MinSize, + MaxSize = resource.MaxSize, + PreferredSize = resource.PreferredSize + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + + public static List ToModel(this IEnumerable resources) + { + return resources.Select(ToModel).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Queue/QueueActionController.cs b/src/Radarr.Api.V4/Queue/QueueActionController.cs new file mode 100644 index 0000000000..f9084c35b2 --- /dev/null +++ b/src/Radarr.Api.V4/Queue/QueueActionController.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Pending; +using Radarr.Http; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Queue +{ + [V4ApiController("queue")] + public class QueueActionController : Controller + { + private readonly IPendingReleaseService _pendingReleaseService; + private readonly IDownloadService _downloadService; + + public QueueActionController(IPendingReleaseService pendingReleaseService, + IDownloadService downloadService) + { + _pendingReleaseService = pendingReleaseService; + _downloadService = downloadService; + } + + [HttpPost("grab/{id:int}")] + public object Grab(int id) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease == null) + { + throw new NotFoundException(); + } + + _downloadService.DownloadReport(pendingRelease.RemoteMovie); + + return new { }; + } + + [HttpPost("grab/bulk")] + public object Grab([FromBody] QueueBulkResource resource) + { + foreach (var id in resource.Ids) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease == null) + { + throw new NotFoundException(); + } + + _downloadService.DownloadReport(pendingRelease.RemoteMovie); + } + + return new { }; + } + } +} diff --git a/src/Radarr.Api.V4/Queue/QueueBulkResource.cs b/src/Radarr.Api.V4/Queue/QueueBulkResource.cs new file mode 100644 index 0000000000..ac2bb1089d --- /dev/null +++ b/src/Radarr.Api.V4/Queue/QueueBulkResource.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Radarr.Api.V4.Queue +{ + public class QueueBulkResource + { + public List Ids { get; set; } + } +} diff --git a/src/Radarr.Api.V4/Queue/QueueController.cs b/src/Radarr.Api.V4/Queue/QueueController.cs new file mode 100644 index 0000000000..946fdd9b9d --- /dev/null +++ b/src/Radarr.Api.V4/Queue/QueueController.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Blocklisting; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Radarr.Http; +using Radarr.Http.Extensions; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V4.Queue +{ + [V4ApiController] + public class QueueController : RestControllerWithSignalR, + IHandle, IHandle + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + + private readonly QualityModelComparer _qualityComparer; + private readonly ITrackedDownloadService _trackedDownloadService; + private readonly IFailedDownloadService _failedDownloadService; + private readonly IIgnoredDownloadService _ignoredDownloadService; + private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IBlocklistService _blocklistService; + + public QueueController(IBroadcastSignalRMessage broadcastSignalRMessage, + IQueueService queueService, + IPendingReleaseService pendingReleaseService, + ProfileService qualityProfileService, + ITrackedDownloadService trackedDownloadService, + IFailedDownloadService failedDownloadService, + IIgnoredDownloadService ignoredDownloadService, + IProvideDownloadClient downloadClientProvider, + IBlocklistService blocklistService) + : base(broadcastSignalRMessage) + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + _trackedDownloadService = trackedDownloadService; + _failedDownloadService = failedDownloadService; + _ignoredDownloadService = ignoredDownloadService; + _downloadClientProvider = downloadClientProvider; + _blocklistService = blocklistService; + + _qualityComparer = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty)); + } + + protected override QueueResource GetResourceById(int id) + { + throw new NotImplementedException(); + } + + [RestDeleteById] + public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false) + { + var trackedDownload = Remove(id, removeFromClient, blocklist); + + if (trackedDownload != null) + { + _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); + } + } + + [HttpDelete("bulk")] + public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false) + { + var trackedDownloadIds = new List(); + + foreach (var id in resource.Ids) + { + var trackedDownload = Remove(id, removeFromClient, blocklist); + + if (trackedDownload != null) + { + trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); + } + } + + _trackedDownloadService.StopTracking(trackedDownloadIds); + + return new { }; + } + + [HttpGet] + public PagingResource GetQueue(bool includeUnknownMovieItems = false, bool includeMovie = false) + { + var pagingResource = Request.ReadPagingResourceFromRequest(); + var pagingSpec = pagingResource.MapToPagingSpec("timeleft", SortDirection.Ascending); + + return pagingSpec.ApplyToPage((spec) => GetQueue(spec, includeUnknownMovieItems), (q) => MapToResource(q, includeMovie)); + } + + private PagingSpec GetQueue(PagingSpec pagingSpec, bool includeUnknownMovieItems) + { + var ascending = pagingSpec.SortDirection == SortDirection.Ascending; + var orderByFunc = GetOrderByFunc(pagingSpec); + + var queue = _queueService.GetQueue(); + var filteredQueue = includeUnknownMovieItems ? queue : queue.Where(q => q.Movie != null); + var pending = _pendingReleaseService.GetPendingQueue(); + var fullQueue = filteredQueue.Concat(pending).ToList(); + IOrderedEnumerable ordered; + + if (pagingSpec.SortKey == "timeleft") + { + ordered = ascending + ? fullQueue.OrderBy(q => q.Timeleft, new TimeleftComparer()) + : fullQueue.OrderByDescending(q => q.Timeleft, new TimeleftComparer()); + } + else if (pagingSpec.SortKey == "estimatedCompletionTime") + { + ordered = ascending + ? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()) + : fullQueue.OrderByDescending(q => q.EstimatedCompletionTime, + new EstimatedCompletionTimeComparer()); + } + else if (pagingSpec.SortKey == "protocol") + { + ordered = ascending + ? fullQueue.OrderBy(q => q.Protocol) + : fullQueue.OrderByDescending(q => q.Protocol); + } + else if (pagingSpec.SortKey == "indexer") + { + ordered = ascending + ? fullQueue.OrderBy(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase) + : fullQueue.OrderByDescending(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase); + } + else if (pagingSpec.SortKey == "downloadClient") + { + ordered = ascending + ? fullQueue.OrderBy(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase) + : fullQueue.OrderByDescending(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase); + } + else if (pagingSpec.SortKey == "quality") + { + ordered = ascending + ? fullQueue.OrderBy(q => q.Quality, _qualityComparer) + : fullQueue.OrderByDescending(q => q.Quality, _qualityComparer); + } + else if (pagingSpec.SortKey == "languages") + { + ordered = ascending + ? fullQueue.OrderBy(q => q.Languages, new LanguagesComparer()) + : fullQueue.OrderByDescending(q => q.Languages, new LanguagesComparer()); + } + else + { + ordered = ascending ? fullQueue.OrderBy(orderByFunc) : fullQueue.OrderByDescending(orderByFunc); + } + + ordered = ordered.ThenByDescending(q => q.Size == 0 ? 0 : 100 - (q.Sizeleft / q.Size * 100)); + + pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); + pagingSpec.TotalRecords = fullQueue.Count; + + if (pagingSpec.Records.Empty() && pagingSpec.Page > 1) + { + pagingSpec.Page = (int)Math.Max(Math.Ceiling((decimal)(pagingSpec.TotalRecords / pagingSpec.PageSize)), 1); + pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); + } + + return pagingSpec; + } + + private Func GetOrderByFunc(PagingSpec pagingSpec) + { + switch (pagingSpec.SortKey) + { + case "status": + return q => q.Status; + case "movies.sortTitle": + return q => q.Movie?.MovieMetadata.Value.SortTitle ?? string.Empty; + case "title": + return q => q.Title; + case "languages": + return q => q.Languages; + case "quality": + return q => q.Quality; + case "progress": + // Avoid exploding if a download's size is 0 + return q => 100 - (q.Sizeleft / Math.Max(q.Size * 100, 1)); + default: + return q => q.Timeleft; + } + } + + private TrackedDownload Remove(int id, bool removeFromClient, bool blocklist) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease != null) + { + if (blocklist) + { + _blocklistService.Block(pendingRelease.RemoteMovie, "Pending release manually blocklisted"); + } + + _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); + + return null; + } + + var trackedDownload = GetTrackedDownload(id); + + if (trackedDownload == null) + { + throw new NotFoundException(); + } + + if (removeFromClient) + { + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + + if (downloadClient == null) + { + throw new BadRequestException(); + } + + downloadClient.RemoveItem(trackedDownload.DownloadItem, true); + } + + if (blocklist) + { + _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId); + } + + if (!removeFromClient && !blocklist) + { + if (!_ignoredDownloadService.IgnoreDownload(trackedDownload)) + { + return null; + } + } + + return trackedDownload; + } + + private TrackedDownload GetTrackedDownload(int queueId) + { + var queueItem = _queueService.Find(queueId); + + if (queueItem == null) + { + throw new NotFoundException(); + } + + var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId); + + if (trackedDownload == null) + { + throw new NotFoundException(); + } + + return trackedDownload; + } + + private QueueResource MapToResource(NzbDrone.Core.Queue.Queue queueItem, bool includeMovie) + { + return queueItem.ToResource(includeMovie); + } + + [NonAction] + public void Handle(QueueUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + + [NonAction] + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Radarr.Api.V4/Queue/QueueDetailsController.cs b/src/Radarr.Api.V4/Queue/QueueDetailsController.cs new file mode 100644 index 0000000000..872cd872d0 --- /dev/null +++ b/src/Radarr.Api.V4/Queue/QueueDetailsController.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Radarr.Http; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Queue +{ + [V4ApiController("queue/details")] + public class QueueDetailsController : RestControllerWithSignalR, + IHandle, IHandle + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + + public QueueDetailsController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage) + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + } + + protected override QueueResource GetResourceById(int id) + { + throw new NotImplementedException(); + } + + [HttpGet] + public List GetQueue(int? movieId, bool includeMovie = false) + { + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + var fullQueue = queue.Concat(pending); + + if (movieId.HasValue) + { + return fullQueue.Where(q => q.Movie?.Id == movieId.Value).ToResource(includeMovie); + } + + return fullQueue.ToResource(includeMovie); + } + + [NonAction] + public void Handle(QueueUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + + [NonAction] + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Radarr.Api.V4/Queue/QueueResource.cs b/src/Radarr.Api.V4/Queue/QueueResource.cs new file mode 100644 index 0000000000..43689c8389 --- /dev/null +++ b/src/Radarr.Api.V4/Queue/QueueResource.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Download.TrackedDownloads; +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.Queue +{ + public class QueueResource : RestResource + { + public int? MovieId { get; set; } + public MovieResource Movie { get; set; } + public List Languages { get; set; } + public QualityModel Quality { get; set; } + public List CustomFormats { get; set; } + public decimal Size { get; set; } + public string Title { get; set; } + public decimal Sizeleft { get; set; } + public TimeSpan? Timeleft { get; set; } + public DateTime? EstimatedCompletionTime { get; set; } + public string Status { get; set; } + public TrackedDownloadStatus? TrackedDownloadStatus { get; set; } + public TrackedDownloadState? TrackedDownloadState { get; set; } + public List StatusMessages { get; set; } + public string ErrorMessage { get; set; } + public string DownloadId { get; set; } + public DownloadProtocol Protocol { get; set; } + public string DownloadClient { get; set; } + public string Indexer { get; set; } + public string OutputPath { get; set; } + } + + public static class QueueResourceMapper + { + public static QueueResource ToResource(this NzbDrone.Core.Queue.Queue model, bool includeMovie) + { + if (model == null) + { + return null; + } + + return new QueueResource + { + Id = model.Id, + MovieId = model.Movie?.Id, + Movie = includeMovie && model.Movie != null ? model.Movie.ToResource(0) : null, + Languages = model.Languages, + Quality = model.Quality, + CustomFormats = model.RemoteMovie?.CustomFormats?.ToResource(false), + Size = model.Size, + Title = model.Title, + Sizeleft = model.Sizeleft, + Timeleft = model.Timeleft, + EstimatedCompletionTime = model.EstimatedCompletionTime, + Status = model.Status.FirstCharToLower(), + TrackedDownloadStatus = model.TrackedDownloadStatus, + TrackedDownloadState = model.TrackedDownloadState, + StatusMessages = model.StatusMessages, + ErrorMessage = model.ErrorMessage, + DownloadId = model.DownloadId, + Protocol = model.Protocol, + DownloadClient = model.DownloadClient, + Indexer = model.Indexer, + OutputPath = model.OutputPath + }; + } + + public static List ToResource(this IEnumerable models, bool includeMovie) + { + return models.Select((m) => ToResource(m, includeMovie)).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Queue/QueueStatusController.cs b/src/Radarr.Api.V4/Queue/QueueStatusController.cs new file mode 100644 index 0000000000..35bd3db9a1 --- /dev/null +++ b/src/Radarr.Api.V4/Queue/QueueStatusController.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.TPL; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Radarr.Http; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Queue +{ + [V4ApiController("queue/status")] + public class QueueStatusController : RestControllerWithSignalR, + IHandle, IHandle + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + private readonly Debouncer _broadcastDebounce; + + public QueueStatusController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage) + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + + _broadcastDebounce = new Debouncer(BroadcastChange, TimeSpan.FromSeconds(5)); + } + + protected override QueueStatusResource GetResourceById(int id) + { + throw new NotImplementedException(); + } + + [HttpGet] + public QueueStatusResource GetQueueStatus() + { + _broadcastDebounce.Pause(); + + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + + var resource = new QueueStatusResource + { + TotalCount = queue.Count + pending.Count, + Count = queue.Count(q => q.Movie != null) + pending.Count, + UnknownCount = queue.Count(q => q.Movie == null), + Errors = queue.Any(q => q.Movie != null && q.TrackedDownloadStatus == TrackedDownloadStatus.Error), + Warnings = queue.Any(q => q.Movie != null && q.TrackedDownloadStatus == TrackedDownloadStatus.Warning), + UnknownErrors = queue.Any(q => q.Movie == null && q.TrackedDownloadStatus == TrackedDownloadStatus.Error), + UnknownWarnings = queue.Any(q => q.Movie == null && q.TrackedDownloadStatus == TrackedDownloadStatus.Warning) + }; + + _broadcastDebounce.Resume(); + + return resource; + } + + private void BroadcastChange() + { + BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); + } + + [NonAction] + public void Handle(QueueUpdatedEvent message) + { + _broadcastDebounce.Execute(); + } + + [NonAction] + public void Handle(PendingReleasesUpdatedEvent message) + { + _broadcastDebounce.Execute(); + } + } +} diff --git a/src/Radarr.Api.V4/Queue/QueueStatusResource.cs b/src/Radarr.Api.V4/Queue/QueueStatusResource.cs new file mode 100644 index 0000000000..6c7329d749 --- /dev/null +++ b/src/Radarr.Api.V4/Queue/QueueStatusResource.cs @@ -0,0 +1,15 @@ +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Queue +{ + public class QueueStatusResource : RestResource + { + public int TotalCount { get; set; } + public int Count { get; set; } + public int UnknownCount { get; set; } + public bool Errors { get; set; } + public bool Warnings { get; set; } + public bool UnknownErrors { get; set; } + public bool UnknownWarnings { get; set; } + } +} diff --git a/src/Radarr.Api.V4/Radarr.Api.V4.csproj b/src/Radarr.Api.V4/Radarr.Api.V4.csproj new file mode 100644 index 0000000000..44e8380fb4 --- /dev/null +++ b/src/Radarr.Api.V4/Radarr.Api.V4.csproj @@ -0,0 +1,14 @@ + + + net6.0 + + + + + + + + + + + diff --git a/src/Radarr.Api.V4/RemotePathMappings/RemotePathMappingController.cs b/src/Radarr.Api.V4/RemotePathMappings/RemotePathMappingController.cs new file mode 100644 index 0000000000..6e58383005 --- /dev/null +++ b/src/Radarr.Api.V4/RemotePathMappings/RemotePathMappingController.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.Validation.Paths; +using Radarr.Http; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V4.RemotePathMappings +{ + [V4ApiController] + public class RemotePathMappingController : RestController + { + private readonly IRemotePathMappingService _remotePathMappingService; + + public RemotePathMappingController(IRemotePathMappingService remotePathMappingService, + PathExistsValidator pathExistsValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator) + { + _remotePathMappingService = remotePathMappingService; + + SharedValidator.RuleFor(c => c.Host) + .NotEmpty(); + + // We cannot use IsValidPath here, because it's a remote path, possibly other OS. + SharedValidator.RuleFor(c => c.RemotePath) + .NotEmpty(); + + SharedValidator.RuleFor(c => c.LocalPath) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(pathExistsValidator); + } + + protected override RemotePathMappingResource GetResourceById(int id) + { + return _remotePathMappingService.Get(id).ToResource(); + } + + [RestPostById] + public ActionResult CreateMapping(RemotePathMappingResource resource) + { + var model = resource.ToModel(); + + return Created(_remotePathMappingService.Add(model).Id); + } + + [HttpGet] + public List GetMappings() + { + return _remotePathMappingService.All().ToResource(); + } + + [RestDeleteById] + public void DeleteMapping(int id) + { + _remotePathMappingService.Remove(id); + } + + [RestPutById] + public ActionResult UpdateMapping(RemotePathMappingResource resource) + { + var mapping = resource.ToModel(); + + return Accepted(_remotePathMappingService.Update(mapping)); + } + } +} diff --git a/src/Radarr.Api.V4/RemotePathMappings/RemotePathMappingResource.cs b/src/Radarr.Api.V4/RemotePathMappings/RemotePathMappingResource.cs new file mode 100644 index 0000000000..f84e21bf4d --- /dev/null +++ b/src/Radarr.Api.V4/RemotePathMappings/RemotePathMappingResource.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.RemotePathMappings; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.RemotePathMappings +{ + public class RemotePathMappingResource : RestResource + { + public string Host { get; set; } + public string RemotePath { get; set; } + public string LocalPath { get; set; } + } + + public static class RemotePathMappingResourceMapper + { + public static RemotePathMappingResource ToResource(this RemotePathMapping model) + { + if (model == null) + { + return null; + } + + return new RemotePathMappingResource + { + Id = model.Id, + + Host = model.Host, + RemotePath = model.RemotePath, + LocalPath = model.LocalPath + }; + } + + public static RemotePathMapping ToModel(this RemotePathMappingResource resource) + { + if (resource == null) + { + return null; + } + + return new RemotePathMapping + { + Id = resource.Id, + + Host = resource.Host, + RemotePath = resource.RemotePath, + LocalPath = resource.LocalPath + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Restrictions/RestrictionController.cs b/src/Radarr.Api.V4/Restrictions/RestrictionController.cs new file mode 100644 index 0000000000..eda6e01917 --- /dev/null +++ b/src/Radarr.Api.V4/Restrictions/RestrictionController.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Restrictions; +using Radarr.Http; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V4.Restrictions +{ + [V4ApiController] + public class RestrictionController : RestController + { + private readonly IRestrictionService _restrictionService; + + public RestrictionController(IRestrictionService restrictionService) + { + _restrictionService = restrictionService; + + SharedValidator.RuleFor(d => d).Custom((restriction, context) => + { + if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace()) + { + context.AddFailure("Either 'Must contain' or 'Must not contain' is required"); + } + }); + } + + protected override RestrictionResource GetResourceById(int id) + { + return _restrictionService.Get(id).ToResource(); + } + + [HttpGet] + public List GetAll() + { + return _restrictionService.All().ToResource(); + } + + [RestPostById] + public ActionResult Create(RestrictionResource resource) + { + return Created(_restrictionService.Add(resource.ToModel()).Id); + } + + [RestPutById] + public ActionResult Update(RestrictionResource resource) + { + _restrictionService.Update(resource.ToModel()); + return Accepted(resource.Id); + } + + [RestDeleteById] + public void DeleteRestriction(int id) + { + _restrictionService.Delete(id); + } + } +} diff --git a/src/Radarr.Api.V4/Restrictions/RestrictionResource.cs b/src/Radarr.Api.V4/Restrictions/RestrictionResource.cs new file mode 100644 index 0000000000..9e72f2b203 --- /dev/null +++ b/src/Radarr.Api.V4/Restrictions/RestrictionResource.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Restrictions; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Restrictions +{ + public class RestrictionResource : RestResource + { + public string Required { get; set; } + public string Preferred { get; set; } + public string Ignored { get; set; } + public HashSet Tags { get; set; } + + public RestrictionResource() + { + Tags = new HashSet(); + } + } + + public static class RestrictionResourceMapper + { + public static RestrictionResource ToResource(this Restriction model) + { + if (model == null) + { + return null; + } + + return new RestrictionResource + { + Id = model.Id, + + Required = model.Required, + Preferred = model.Preferred, + Ignored = model.Ignored, + Tags = new HashSet(model.Tags) + }; + } + + public static Restriction ToModel(this RestrictionResource resource) + { + if (resource == null) + { + return null; + } + + return new Restriction + { + Id = resource.Id, + + Required = resource.Required, + Preferred = resource.Preferred, + Ignored = resource.Ignored, + Tags = new HashSet(resource.Tags) + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/RootFolders/RootFolderController.cs b/src/Radarr.Api.V4/RootFolders/RootFolderController.cs new file mode 100644 index 0000000000..e269115515 --- /dev/null +++ b/src/Radarr.Api.V4/RootFolders/RootFolderController.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; +using Radarr.Http; +using Radarr.Http.Extensions; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V4.RootFolders +{ + [V4ApiController] + public class RootFolderController : RestControllerWithSignalR + { + private readonly IRootFolderService _rootFolderService; + + public RootFolderController(IRootFolderService rootFolderService, + IBroadcastSignalRMessage signalRBroadcaster, + RootFolderValidator rootFolderValidator, + PathExistsValidator pathExistsValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator, + RecycleBinValidator recycleBinValidator, + StartupFolderValidator startupFolderValidator, + SystemFolderValidator systemFolderValidator, + FolderWritableValidator folderWritableValidator) + : base(signalRBroadcaster) + { + _rootFolderService = rootFolderService; + + SharedValidator.RuleFor(c => c.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(startupFolderValidator) + .SetValidator(recycleBinValidator) + .SetValidator(pathExistsValidator) + .SetValidator(systemFolderValidator) + .SetValidator(folderWritableValidator); + } + + protected override RootFolderResource GetResourceById(int id) + { + var timeout = Request?.GetBooleanQueryParameter("timeout", true) ?? true; + + return _rootFolderService.Get(id, timeout).ToResource(); + } + + [RestPostById] + public ActionResult CreateRootFolder(RootFolderResource rootFolderResource) + { + var model = rootFolderResource.ToModel(); + + return Created(_rootFolderService.Add(model).Id); + } + + [HttpGet] + public List GetRootFolders() + { + return _rootFolderService.AllWithUnmappedFolders().ToResource(); + } + + [RestDeleteById] + public void DeleteFolder(int id) + { + _rootFolderService.Remove(id); + } + } +} diff --git a/src/Radarr.Api.V4/RootFolders/RootFolderResource.cs b/src/Radarr.Api.V4/RootFolders/RootFolderResource.cs new file mode 100644 index 0000000000..e9fec65455 --- /dev/null +++ b/src/Radarr.Api.V4/RootFolders/RootFolderResource.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.RootFolders; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.RootFolders +{ + public class RootFolderResource : RestResource + { + public string Path { get; set; } + public bool Accessible { get; set; } + public long? FreeSpace { get; set; } + + public List UnmappedFolders { get; set; } + } + + public static class RootFolderResourceMapper + { + public static RootFolderResource ToResource(this RootFolder model) + { + if (model == null) + { + return null; + } + + return new RootFolderResource + { + Id = model.Id, + + Path = model.Path.GetCleanPath(), + Accessible = model.Accessible, + FreeSpace = model.FreeSpace, + UnmappedFolders = model.UnmappedFolders + }; + } + + public static RootFolder ToModel(this RootFolderResource resource) + { + if (resource == null) + { + return null; + } + + return new RootFolder + { + Id = resource.Id, + + Path = resource.Path + + // Accessible + // FreeSpace + // UnmappedFolders + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/System/Backup/BackupController.cs b/src/Radarr.Api.V4/System/Backup/BackupController.cs new file mode 100644 index 0000000000..da67a6afd2 --- /dev/null +++ b/src/Radarr.Api.V4/System/Backup/BackupController.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Crypto; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Backup; +using Radarr.Http; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V4.System.Backup +{ + [V4ApiController("system/backup")] + public class BackupController : Controller + { + private readonly IBackupService _backupService; + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + + private static readonly List ValidExtensions = new List { ".zip", ".db", ".xml" }; + + public BackupController(IBackupService backupService, + IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider) + { + _backupService = backupService; + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + } + + [HttpGet] + public List GetBackupFiles() + { + var backups = _backupService.GetBackups(); + + return backups.Select(b => new BackupResource + { + Id = GetBackupId(b), + Name = b.Name, + Path = $"/backup/{b.Type.ToString().ToLower()}/{b.Name}", + Type = b.Type, + Size = b.Size, + Time = b.Time + }) + .OrderByDescending(b => b.Time) + .ToList(); + } + + [RestDeleteById] + public void DeleteBackup(int id) + { + var backup = GetBackup(id); + var path = GetBackupPath(backup); + + if (!_diskProvider.FileExists(path)) + { + throw new NotFoundException(); + } + + _diskProvider.DeleteFile(path); + } + + [HttpPost("restore/{id:int}")] + public object Restore(int id) + { + var backup = GetBackup(id); + + if (backup == null) + { + throw new NotFoundException(); + } + + var path = GetBackupPath(backup); + + _backupService.Restore(path); + + return new + { + RestartRequired = true + }; + } + + [HttpPost("restore/upload")] + public object UploadAndRestore() + { + var files = Request.Form.Files; + + if (files.Empty()) + { + throw new BadRequestException("file must be provided"); + } + + var file = files[0]; + var extension = Path.GetExtension(file.FileName); + + if (!ValidExtensions.Contains(extension)) + { + throw new UnsupportedMediaTypeException($"Invalid extension, must be one of: {ValidExtensions.Join(", ")}"); + } + + var path = Path.Combine(_appFolderInfo.TempFolder, $"radarr_backup_restore{extension}"); + + _diskProvider.SaveStream(file.OpenReadStream(), path); + _backupService.Restore(path); + + // Cleanup restored file + _diskProvider.DeleteFile(path); + + return new + { + RestartRequired = true + }; + } + + private string GetBackupPath(NzbDrone.Core.Backup.Backup backup) + { + return Path.Combine(_backupService.GetBackupFolder(backup.Type), backup.Name); + } + + private int GetBackupId(NzbDrone.Core.Backup.Backup backup) + { + return HashConverter.GetHashInt31($"backup-{backup.Type}-{backup.Name}"); + } + + private NzbDrone.Core.Backup.Backup GetBackup(int id) + { + return _backupService.GetBackups().SingleOrDefault(b => GetBackupId(b) == id); + } + } +} diff --git a/src/Radarr.Api.V4/System/Backup/BackupResource.cs b/src/Radarr.Api.V4/System/Backup/BackupResource.cs new file mode 100644 index 0000000000..d2e9d05a9f --- /dev/null +++ b/src/Radarr.Api.V4/System/Backup/BackupResource.cs @@ -0,0 +1,15 @@ +using System; +using NzbDrone.Core.Backup; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.System.Backup +{ + public class BackupResource : RestResource + { + public string Name { get; set; } + public string Path { get; set; } + public BackupType Type { get; set; } + public long Size { get; set; } + public DateTime Time { get; set; } + } +} diff --git a/src/Radarr.Api.V4/System/SystemController.cs b/src/Radarr.Api.V4/System/SystemController.cs new file mode 100644 index 0000000000..f3ffdae71e --- /dev/null +++ b/src/Radarr.Api.V4/System/SystemController.cs @@ -0,0 +1,126 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Internal; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Lifecycle; +using Radarr.Http; +using Radarr.Http.Validation; + +namespace Radarr.Api.V4.System +{ + [V4ApiController] + public class SystemController : Controller + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IRuntimeInfo _runtimeInfo; + private readonly IPlatformInfo _platformInfo; + private readonly IOsInfo _osInfo; + private readonly IConfigFileProvider _configFileProvider; + private readonly IMainDatabase _database; + private readonly ILifecycleService _lifecycleService; + private readonly IDeploymentInfoProvider _deploymentInfoProvider; + private readonly EndpointDataSource _endpointData; + private readonly DfaGraphWriter _graphWriter; + private readonly DuplicateEndpointDetector _detector; + + public SystemController(IAppFolderInfo appFolderInfo, + IRuntimeInfo runtimeInfo, + IPlatformInfo platformInfo, + IOsInfo osInfo, + IConfigFileProvider configFileProvider, + IMainDatabase database, + ILifecycleService lifecycleService, + IDeploymentInfoProvider deploymentInfoProvider, + EndpointDataSource endpoints, + DfaGraphWriter graphWriter, + DuplicateEndpointDetector detector) + { + _appFolderInfo = appFolderInfo; + _runtimeInfo = runtimeInfo; + _platformInfo = platformInfo; + _osInfo = osInfo; + _configFileProvider = configFileProvider; + _database = database; + _lifecycleService = lifecycleService; + _deploymentInfoProvider = deploymentInfoProvider; + _endpointData = endpoints; + _graphWriter = graphWriter; + _detector = detector; + } + + [HttpGet("status")] + public object GetStatus() + { + return new + { + AppName = BuildInfo.AppName, + InstanceName = _configFileProvider.InstanceName, + Version = BuildInfo.Version.ToString(), + BuildTime = BuildInfo.BuildDateTime, + IsDebug = BuildInfo.IsDebug, + IsProduction = RuntimeInfo.IsProduction, + IsAdmin = _runtimeInfo.IsAdmin, + IsUserInteractive = RuntimeInfo.IsUserInteractive, + StartupPath = _appFolderInfo.StartUpFolder, + AppData = _appFolderInfo.GetAppDataPath(), + OsName = _osInfo.Name, + OsVersion = _osInfo.Version, + IsNetCore = true, + IsLinux = OsInfo.IsLinux, + IsOsx = OsInfo.IsOsx, + IsWindows = OsInfo.IsWindows, + IsDocker = _osInfo.IsDocker, + Mode = _runtimeInfo.Mode, + Branch = _configFileProvider.Branch, + Authentication = _configFileProvider.AuthenticationMethod, + DatabaseType = _database.DatabaseType, + DatabaseVersion = _database.Version, + MigrationVersion = _database.Migration, + UrlBase = _configFileProvider.UrlBase, + RuntimeVersion = _platformInfo.Version, + RuntimeName = "netcore", + StartTime = _runtimeInfo.StartTime, + PackageVersion = _deploymentInfoProvider.PackageVersion, + PackageAuthor = _deploymentInfoProvider.PackageAuthor, + PackageUpdateMechanism = _deploymentInfoProvider.PackageUpdateMechanism, + PackageUpdateMechanismMessage = _deploymentInfoProvider.PackageUpdateMechanismMessage + }; + } + + [HttpGet("routes")] + public IActionResult GetRoutes() + { + using (var sw = new StringWriter()) + { + _graphWriter.Write(_endpointData, sw); + var graph = sw.ToString(); + return Content(graph, "text/plain"); + } + } + + [HttpGet("routes/duplicate")] + public object DuplicateRoutes() + { + return _detector.GetDuplicateEndpoints(_endpointData); + } + + [HttpPost("shutdown")] + public object Shutdown() + { + Task.Factory.StartNew(() => _lifecycleService.Shutdown()); + return new { ShuttingDown = true }; + } + + [HttpPost("restart")] + public object Restart() + { + Task.Factory.StartNew(() => _lifecycleService.Restart()); + return new { Restarting = true }; + } + } +} diff --git a/src/Radarr.Api.V4/System/Tasks/TaskController.cs b/src/Radarr.Api.V4/System/Tasks/TaskController.cs new file mode 100644 index 0000000000..5973aa89aa --- /dev/null +++ b/src/Radarr.Api.V4/System/Tasks/TaskController.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Jobs; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.SignalR; +using Radarr.Http; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.System.Tasks +{ + [V4ApiController("system/task")] + public class TaskController : RestControllerWithSignalR, IHandle + { + private readonly ITaskManager _taskManager; + + public TaskController(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage) + : base(broadcastSignalRMessage) + { + _taskManager = taskManager; + } + + [HttpGet] + public List GetAll() + { + return _taskManager.GetAll() + .Select(ConvertToResource) + .OrderBy(t => t.Name) + .ToList(); + } + + protected override TaskResource GetResourceById(int id) + { + var task = _taskManager.GetAll() + .SingleOrDefault(t => t.Id == id); + + if (task == null) + { + return null; + } + + return ConvertToResource(task); + } + + private static TaskResource ConvertToResource(ScheduledTask scheduledTask) + { + var taskName = scheduledTask.TypeName.Split('.').Last().Replace("Command", ""); + + return new TaskResource + { + Id = scheduledTask.Id, + Name = taskName.SplitCamelCase(), + TaskName = taskName, + Interval = scheduledTask.Interval, + LastExecution = scheduledTask.LastExecution, + LastStartTime = scheduledTask.LastStartTime, + NextExecution = scheduledTask.LastExecution.AddMinutes(scheduledTask.Interval) + }; + } + + [NonAction] + public void Handle(CommandExecutedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Radarr.Api.V4/System/Tasks/TaskResource.cs b/src/Radarr.Api.V4/System/Tasks/TaskResource.cs new file mode 100644 index 0000000000..d51f5ae4c3 --- /dev/null +++ b/src/Radarr.Api.V4/System/Tasks/TaskResource.cs @@ -0,0 +1,17 @@ +using System; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.System.Tasks +{ + public class TaskResource : RestResource + { + public string Name { get; set; } + public string TaskName { get; set; } + public int Interval { get; set; } + public DateTime LastExecution { get; set; } + public DateTime LastStartTime { get; set; } + public DateTime NextExecution { get; set; } + + public TimeSpan LastDuration => LastExecution - LastStartTime; + } +} diff --git a/src/Radarr.Api.V4/Tags/TagController.cs b/src/Radarr.Api.V4/Tags/TagController.cs new file mode 100644 index 0000000000..9af3f5fd2b --- /dev/null +++ b/src/Radarr.Api.V4/Tags/TagController.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tags; +using NzbDrone.SignalR; +using Radarr.Http; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V4.Tags +{ + [V4ApiController] + public class TagController : RestControllerWithSignalR, IHandle + { + private readonly ITagService _tagService; + + public TagController(IBroadcastSignalRMessage signalRBroadcaster, + ITagService tagService) + : base(signalRBroadcaster) + { + _tagService = tagService; + } + + protected override TagResource GetResourceById(int id) + { + return _tagService.GetTag(id).ToResource(); + } + + [HttpGet] + public List GetAll() + { + return _tagService.All().ToResource(); + } + + [RestPostById] + public ActionResult Create(TagResource resource) + { + return Created(_tagService.Add(resource.ToModel()).Id); + } + + [RestPutById] + public ActionResult Update(TagResource resource) + { + _tagService.Update(resource.ToModel()); + return Accepted(resource.Id); + } + + [RestDeleteById] + public void DeleteTag(int id) + { + _tagService.Delete(id); + } + + [NonAction] + public void Handle(TagsUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Radarr.Api.V4/Tags/TagDetailsController.cs b/src/Radarr.Api.V4/Tags/TagDetailsController.cs new file mode 100644 index 0000000000..4b5fc1d34e --- /dev/null +++ b/src/Radarr.Api.V4/Tags/TagDetailsController.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Tags; +using Radarr.Http; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Tags +{ + [V4ApiController("tag/detail")] + public class TagDetailsController : RestController + { + private readonly ITagService _tagService; + + public TagDetailsController(ITagService tagService) + { + _tagService = tagService; + } + + protected override TagDetailsResource GetResourceById(int id) + { + return _tagService.Details(id).ToResource(); + } + + [HttpGet] + public List GetAll() + { + return _tagService.Details().ToResource(); + } + } +} diff --git a/src/Radarr.Api.V4/Tags/TagDetailsResource.cs b/src/Radarr.Api.V4/Tags/TagDetailsResource.cs new file mode 100644 index 0000000000..dcaeae9887 --- /dev/null +++ b/src/Radarr.Api.V4/Tags/TagDetailsResource.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Tags; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Tags +{ + public class TagDetailsResource : RestResource + { + public string Label { get; set; } + public List DelayProfileIds { get; set; } + public List NotificationIds { get; set; } + public List RestrictionIds { get; set; } + public List ImportListIds { get; set; } + public List MovieIds { get; set; } + public List IndexerIds { get; set; } + } + + public static class TagDetailsResourceMapper + { + public static TagDetailsResource ToResource(this TagDetails model) + { + if (model == null) + { + return null; + } + + return new TagDetailsResource + { + Id = model.Id, + Label = model.Label, + DelayProfileIds = model.DelayProfileIds, + NotificationIds = model.NotificationIds, + RestrictionIds = model.RestrictionIds, + ImportListIds = model.ImportListIds, + MovieIds = model.MovieIds, + IndexerIds = model.IndexerIds + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Tags/TagResource.cs b/src/Radarr.Api.V4/Tags/TagResource.cs new file mode 100644 index 0000000000..9736d23b60 --- /dev/null +++ b/src/Radarr.Api.V4/Tags/TagResource.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Tags; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Tags +{ + public class TagResource : RestResource + { + public string Label { get; set; } + } + + public static class TagResourceMapper + { + public static TagResource ToResource(this Tag model) + { + if (model == null) + { + return null; + } + + return new TagResource + { + Id = model.Id, + Label = model.Label + }; + } + + public static Tag ToModel(this TagResource resource) + { + if (resource == null) + { + return null; + } + + return new Tag + { + Id = resource.Id, + Label = resource.Label + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/Update/UpdateController.cs b/src/Radarr.Api.V4/Update/UpdateController.cs new file mode 100644 index 0000000000..eea7a96680 --- /dev/null +++ b/src/Radarr.Api.V4/Update/UpdateController.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Update; +using NzbDrone.Core.Update.History; +using Radarr.Http; + +namespace Radarr.Api.V4.Update +{ + [V4ApiController] + public class UpdateController : Controller + { + private readonly IRecentUpdateProvider _recentUpdateProvider; + private readonly IUpdateHistoryService _updateHistoryService; + + public UpdateController(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService) + { + _recentUpdateProvider = recentUpdateProvider; + _updateHistoryService = updateHistoryService; + } + + [HttpGet] + public List GetRecentUpdates() + { + var resources = _recentUpdateProvider.GetRecentUpdatePackages() + .OrderByDescending(u => u.Version) + .ToResource(); + + if (resources.Any()) + { + var first = resources.First(); + first.Latest = true; + + if (first.Version > BuildInfo.Version) + { + first.Installable = true; + } + + var installed = resources.SingleOrDefault(r => r.Version == BuildInfo.Version); + + if (installed != null) + { + installed.Installed = true; + } + + var installDates = _updateHistoryService.InstalledSince(resources.Last().ReleaseDate) + .DistinctBy(v => v.Version) + .ToDictionary(v => v.Version); + + foreach (var resource in resources) + { + if (installDates.TryGetValue(resource.Version, out var installDate)) + { + resource.InstalledOn = installDate.Date; + } + } + } + + return resources; + } + } +} diff --git a/src/Radarr.Api.V4/Update/UpdateResource.cs b/src/Radarr.Api.V4/Update/UpdateResource.cs new file mode 100644 index 0000000000..88e5cc01bf --- /dev/null +++ b/src/Radarr.Api.V4/Update/UpdateResource.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Update; +using Radarr.Http.REST; + +namespace Radarr.Api.V4.Update +{ + public class UpdateResource : RestResource + { + public Version Version { get; set; } + + public string Branch { get; set; } + public DateTime ReleaseDate { get; set; } + public string FileName { get; set; } + public string Url { get; set; } + public bool Installed { get; set; } + public DateTime? InstalledOn { get; set; } + public bool Installable { get; set; } + public bool Latest { get; set; } + public UpdateChanges Changes { get; set; } + public string Hash { get; set; } + } + + public static class UpdateResourceMapper + { + public static UpdateResource ToResource(this UpdatePackage model) + { + if (model == null) + { + return null; + } + + return new UpdateResource + { + Version = model.Version, + + Branch = model.Branch, + ReleaseDate = model.ReleaseDate, + FileName = model.FileName, + Url = model.Url, + + // Installed + // Installable + // Latest + Changes = model.Changes, + Hash = model.Hash, + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Radarr.Api.V4/openapi.json b/src/Radarr.Api.V4/openapi.json new file mode 100644 index 0000000000..aa8b8de626 --- /dev/null +++ b/src/Radarr.Api.V4/openapi.json @@ -0,0 +1,12685 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Radarr", + "description": "Radarr API docs", + "license": { + "name": "GPL-3.0", + "url": "https://github.com/Radarr/Radarr/blob/develop/LICENSE" + }, + "version": "3.0.0" + }, + "servers": [ + { + "url": "{protocol}://{hostpath}", + "variables": { + "protocol": { + "default": "http", + "enum": [ + "http", + "https" + ] + }, + "hostpath": { + "default": "localhost:7878" + } + } + } + ], + "paths": { + "/api/v3/alttitle": { + "get": { + "tags": [ + "AlternativeTitle" + ], + "parameters": [ + { + "name": "movieId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "movieMetadataId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlternativeTitleResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlternativeTitleResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlternativeTitleResource" + } + } + } + } + } + } + } + }, + "/api/v3/alttitle/{id}": { + "get": { + "tags": [ + "AlternativeTitle" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/AlternativeTitleResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlternativeTitleResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/AlternativeTitleResource" + } + } + } + } + } + } + }, + "/login": { + "post": { + "tags": [ + "Authentication" + ], + "parameters": [ + { + "name": "returnUrl", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "Username": { + "type": "string" + }, + "Password": { + "type": "string" + }, + "RememberMe": { + "type": "string" + } + } + }, + "encoding": { + "Username": { + "style": "form" + }, + "Password": { + "style": "form" + }, + "RememberMe": { + "style": "form" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "StaticResource" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/logout": { + "get": { + "tags": [ + "Authentication" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/system/backup": { + "get": { + "tags": [ + "Backup" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BackupResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BackupResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BackupResource" + } + } + } + } + } + } + } + }, + "/api/v3/system/backup/{id}": { + "delete": { + "tags": [ + "Backup" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/system/backup/restore/{id}": { + "post": { + "tags": [ + "Backup" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/system/backup/restore/upload": { + "post": { + "tags": [ + "Backup" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/blocklist": { + "get": { + "tags": [ + "Blocklist" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/BlocklistResourcePagingResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistResourcePagingResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistResourcePagingResource" + } + } + } + } + } + } + }, + "/api/v3/blocklist/movie": { + "get": { + "tags": [ + "Blocklist" + ], + "parameters": [ + { + "name": "movieId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlocklistResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlocklistResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlocklistResource" + } + } + } + } + } + } + } + }, + "/api/v3/blocklist/{id}": { + "delete": { + "tags": [ + "Blocklist" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/blocklist/bulk": { + "delete": { + "tags": [ + "Blocklist" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistBulkResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistBulkResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/BlocklistBulkResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/calendar": { + "get": { + "tags": [ + "Calendar" + ], + "parameters": [ + { + "name": "start", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "end", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "unmonitored", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "includeArtist", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + } + } + } + } + }, + "/api/v3/calendar/{id}": { + "get": { + "tags": [ + "Calendar" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + } + } + } + }, + "/feed/v3/calendar/radarr.ics": { + "get": { + "tags": [ + "CalendarFeed" + ], + "parameters": [ + { + "name": "pastDays", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 7 + } + }, + { + "name": "futureDays", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 28 + } + }, + { + "name": "tagList", + "in": "query", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "unmonitored", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/collection": { + "get": { + "tags": [ + "Collection" + ], + "parameters": [ + { + "name": "tmdbId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CollectionResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CollectionResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CollectionResource" + } + } + } + } + } + } + }, + "put": { + "tags": [ + "Collection" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionUpdateResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CollectionUpdateResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CollectionUpdateResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/collection/{id}": { + "put": { + "tags": [ + "Collection" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CollectionResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CollectionResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CollectionResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CollectionResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "Collection" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CollectionResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CollectionResource" + } + } + } + } + } + } + }, + "/api/v3/command": { + "post": { + "tags": [ + "Command" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CommandResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CommandResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CommandResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CommandResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "Command" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CommandResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CommandResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CommandResource" + } + } + } + } + } + } + } + }, + "/api/v3/command/{id}": { + "delete": { + "tags": [ + "Command" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "Command" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CommandResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CommandResource" + } + } + } + } + } + } + }, + "/api/v3/credit": { + "get": { + "tags": [ + "Credit" + ], + "parameters": [ + { + "name": "movieId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "movieMetadataId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreditResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreditResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreditResource" + } + } + } + } + } + } + } + }, + "/api/v3/credit/{id}": { + "get": { + "tags": [ + "Credit" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CreditResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreditResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreditResource" + } + } + } + } + } + } + }, + "/api/v3/customfilter": { + "get": { + "tags": [ + "CustomFilter" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFilterResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFilterResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFilterResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "CustomFilter" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + } + } + } + } + } + }, + "/api/v3/customfilter/{id}": { + "put": { + "tags": [ + "CustomFilter" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "CustomFilter" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "CustomFilter" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomFilterResource" + } + } + } + } + } + } + }, + "/api/v3/customformat": { + "post": { + "tags": [ + "CustomFormat" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "CustomFormat" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatResource" + } + } + } + } + } + } + } + }, + "/api/v3/customformat/{id}": { + "put": { + "tags": [ + "CustomFormat" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "CustomFormat" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "CustomFormat" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomFormatResource" + } + } + } + } + } + } + }, + "/api/v3/customformat/schema": { + "get": { + "tags": [ + "CustomFormat" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/delayprofile": { + "post": { + "tags": [ + "DelayProfile" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "DelayProfile" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DelayProfileResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DelayProfileResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DelayProfileResource" + } + } + } + } + } + } + } + }, + "/api/v3/delayprofile/{id}": { + "delete": { + "tags": [ + "DelayProfile" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "put": { + "tags": [ + "DelayProfile" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "DelayProfile" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DelayProfileResource" + } + } + } + } + } + } + }, + "/api/v3/diskspace": { + "get": { + "tags": [ + "DiskSpace" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DiskSpaceResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DiskSpaceResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DiskSpaceResource" + } + } + } + } + } + } + } + }, + "/api/v3/downloadclient": { + "get": { + "tags": [ + "DownloadClient" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "DownloadClient" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + } + } + } + } + }, + "/api/v3/downloadclient/{id}": { + "put": { + "tags": [ + "DownloadClient" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "DownloadClient" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "DownloadClient" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + } + } + } + } + }, + "/api/v3/downloadclient/schema": { + "get": { + "tags": [ + "DownloadClient" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + } + } + } + } + } + }, + "/api/v3/downloadclient/test": { + "post": { + "tags": [ + "DownloadClient" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/downloadclient/testall": { + "post": { + "tags": [ + "DownloadClient" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/downloadclient/action/{name}": { + "post": { + "tags": [ + "DownloadClient" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/config/downloadclient": { + "get": { + "tags": [ + "DownloadClientConfig" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + } + } + } + } + } + }, + "/api/v3/config/downloadclient/{id}": { + "put": { + "tags": [ + "DownloadClientConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "DownloadClientConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DownloadClientConfigResource" + } + } + } + } + } + } + }, + "/api/v3/extrafile": { + "get": { + "tags": [ + "ExtraFile" + ], + "parameters": [ + { + "name": "movieId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExtraFileResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExtraFileResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExtraFileResource" + } + } + } + } + } + } + } + }, + "/api/v3/filesystem": { + "get": { + "tags": [ + "FileSystem" + ], + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "includeFiles", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "allowFoldersWithoutTrailingSlashes", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/filesystem/type": { + "get": { + "tags": [ + "FileSystem" + ], + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/filesystem/mediafiles": { + "get": { + "tags": [ + "FileSystem" + ], + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/health": { + "get": { + "tags": [ + "Health" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthResource" + } + } + } + } + } + } + } + }, + "/api/v3/health/{id}": { + "get": { + "tags": [ + "Health" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/HealthResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/HealthResource" + } + } + } + } + } + } + }, + "/api/v3/history": { + "get": { + "tags": [ + "History" + ], + "parameters": [ + { + "name": "includeMovie", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/HistoryResourcePagingResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistoryResourcePagingResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/HistoryResourcePagingResource" + } + } + } + } + } + } + }, + "/api/v3/history/since": { + "get": { + "tags": [ + "History" + ], + "parameters": [ + { + "name": "date", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "eventType", + "in": "query", + "schema": { + "$ref": "#/components/schemas/MovieHistoryEventType" + } + }, + { + "name": "includeMovie", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HistoryResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HistoryResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HistoryResource" + } + } + } + } + } + } + } + }, + "/api/v3/history/movie": { + "get": { + "tags": [ + "History" + ], + "parameters": [ + { + "name": "movieId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "eventType", + "in": "query", + "schema": { + "$ref": "#/components/schemas/MovieHistoryEventType" + } + }, + { + "name": "includeMovie", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HistoryResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HistoryResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HistoryResource" + } + } + } + } + } + } + } + }, + "/api/v3/history/failed/{id}": { + "post": { + "tags": [ + "History" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/config/host": { + "get": { + "tags": [ + "HostConfig" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + } + } + } + } + } + }, + "/api/v3/config/host/{id}": { + "put": { + "tags": [ + "HostConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "HostConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/HostConfigResource" + } + } + } + } + } + } + }, + "/api/v3/exclusions": { + "get": { + "tags": [ + "ImportExclusions" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "ImportExclusions" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + } + } + } + } + }, + "/api/v3/exclusions/{id}": { + "put": { + "tags": [ + "ImportExclusions" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "ImportExclusions" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "ImportExclusions" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + } + } + } + } + }, + "/api/v3/exclusions/bulk": { + "post": { + "tags": [ + "ImportExclusions" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportExclusionsResource" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/importlist": { + "get": { + "tags": [ + "ImportList" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportListResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportListResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportListResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "ImportList" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + } + } + } + } + } + }, + "/api/v3/importlist/{id}": { + "put": { + "tags": [ + "ImportList" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "ImportList" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "ImportList" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + } + } + } + } + } + }, + "/api/v3/importlist/schema": { + "get": { + "tags": [ + "ImportList" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportListResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportListResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportListResource" + } + } + } + } + } + } + } + }, + "/api/v3/importlist/test": { + "post": { + "tags": [ + "ImportList" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/importlist/testall": { + "post": { + "tags": [ + "ImportList" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/importlist/action/{name}": { + "post": { + "tags": [ + "ImportList" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ImportListResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/config/importlist": { + "get": { + "tags": [ + "ImportListConfig" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + } + } + } + } + } + }, + "/api/v3/config/importlist/{id}": { + "put": { + "tags": [ + "ImportListConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "ImportListConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListConfigResource" + } + } + } + } + } + } + }, + "/api/v3/importlist/movie": { + "get": { + "tags": [ + "ImportListMovies" + ], + "parameters": [ + { + "name": "includeRecommendations", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "post": { + "tags": [ + "ImportListMovies" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/indexer": { + "get": { + "tags": [ + "Indexer" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Indexer" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + } + }, + "/api/v3/indexer/{id}": { + "put": { + "tags": [ + "Indexer" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Indexer" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "Indexer" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + } + }, + "/api/v3/indexer/schema": { + "get": { + "tags": [ + "Indexer" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + } + } + }, + "/api/v3/indexer/test": { + "post": { + "tags": [ + "Indexer" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/indexer/testall": { + "post": { + "tags": [ + "Indexer" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/indexer/action/{name}": { + "post": { + "tags": [ + "Indexer" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/config/indexer": { + "get": { + "tags": [ + "IndexerConfig" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + } + } + } + } + } + }, + "/api/v3/config/indexer/{id}": { + "put": { + "tags": [ + "IndexerConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "IndexerConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerConfigResource" + } + } + } + } + } + } + }, + "/api/v3/indexerflag": { + "get": { + "tags": [ + "IndexerFlag" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerFlagResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerFlagResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerFlagResource" + } + } + } + } + } + } + } + }, + "/initialize.js": { + "get": { + "tags": [ + "InitializeJs" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/language": { + "get": { + "tags": [ + "Language" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LanguageResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LanguageResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LanguageResource" + } + } + } + } + } + } + } + }, + "/api/v3/language/{id}": { + "get": { + "tags": [ + "Language" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LanguageResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LanguageResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LanguageResource" + } + } + } + } + } + } + }, + "/api/v3/localization": { + "get": { + "tags": [ + "Localization" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/v3/log": { + "get": { + "tags": [ + "Log" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LogResourcePagingResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LogResourcePagingResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LogResourcePagingResource" + } + } + } + } + } + } + }, + "/api/v3/log/file": { + "get": { + "tags": [ + "LogFile" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogFileResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogFileResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogFileResource" + } + } + } + } + } + } + } + }, + "/api/v3/log/file/{filename}": { + "get": { + "tags": [ + "LogFile" + ], + "parameters": [ + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "pattern": "[-.a-zA-Z0-9]+?\\.txt", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/manualimport": { + "get": { + "tags": [ + "ManualImport" + ], + "parameters": [ + { + "name": "folder", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "downloadId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "movieId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filterExistingFiles", + "in": "query", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManualImportResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManualImportResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManualImportResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "ManualImport" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManualImportReprocessResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManualImportReprocessResource" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManualImportReprocessResource" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/mediacover/{movieId}/{filename}": { + "get": { + "tags": [ + "MediaCover" + ], + "parameters": [ + { + "name": "movieId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "pattern": "(.+)\\.(jpg|png|gif)", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/config/mediamanagement": { + "get": { + "tags": [ + "MediaManagementConfig" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + } + } + } + } + } + }, + "/api/v3/config/mediamanagement/{id}": { + "put": { + "tags": [ + "MediaManagementConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "MediaManagementConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementConfigResource" + } + } + } + } + } + } + }, + "/api/v3/metadata": { + "get": { + "tags": [ + "Metadata" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Metadata" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + } + } + } + }, + "/api/v3/metadata/{id}": { + "put": { + "tags": [ + "Metadata" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Metadata" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "Metadata" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + } + } + } + }, + "/api/v3/metadata/schema": { + "get": { + "tags": [ + "Metadata" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + } + } + } + } + }, + "/api/v3/metadata/test": { + "post": { + "tags": [ + "Metadata" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/metadata/testall": { + "post": { + "tags": [ + "Metadata" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/metadata/action/{name}": { + "post": { + "tags": [ + "Metadata" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/config/metadata": { + "get": { + "tags": [ + "MetadataConfig" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + } + } + } + } + } + }, + "/api/v3/config/metadata/{id}": { + "put": { + "tags": [ + "MetadataConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "MetadataConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MetadataConfigResource" + } + } + } + } + } + } + }, + "/api/v3/movie": { + "get": { + "tags": [ + "Movie" + ], + "parameters": [ + { + "name": "tmdbId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Movie" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + } + } + } + }, + "/api/v3/movie/{id}": { + "put": { + "tags": [ + "Movie" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Movie" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "Movie" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + } + } + } + }, + "/api/v3/movie/editor": { + "put": { + "tags": [ + "MovieEditor" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieEditorResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieEditorResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MovieEditorResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "MovieEditor" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieEditorResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieEditorResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MovieEditorResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/moviefile": { + "get": { + "tags": [ + "MovieFile" + ], + "parameters": [ + { + "name": "movieId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "movieFileIds", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieFileResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieFileResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieFileResource" + } + } + } + } + } + } + } + }, + "/api/v3/moviefile/{id}": { + "put": { + "tags": [ + "MovieFile" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieFileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieFileResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MovieFileResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MovieFileResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieFileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieFileResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "MovieFile" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "MovieFile" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MovieFileResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieFileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieFileResource" + } + } + } + } + } + } + }, + "/api/v3/moviefile/editor": { + "put": { + "tags": [ + "MovieFile" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieFileListResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieFileListResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MovieFileListResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/moviefile/bulk": { + "delete": { + "tags": [ + "MovieFile" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieFileListResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieFileListResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MovieFileListResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/movie/import": { + "post": { + "tags": [ + "MovieImport" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/movie/import/{id}": { + "get": { + "tags": [ + "MovieImport" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + } + } + } + }, + "/api/v3/movie/lookup/tmdb": { + "get": { + "tags": [ + "MovieLookup" + ], + "parameters": [ + { + "name": "tmdbId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/movie/lookup/imdb": { + "get": { + "tags": [ + "MovieLookup" + ], + "parameters": [ + { + "name": "imdbId", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/movie/lookup": { + "get": { + "tags": [ + "MovieLookup" + ], + "parameters": [ + { + "name": "term", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/movie/lookup/{id}": { + "get": { + "tags": [ + "MovieLookup" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MovieResource" + } + } + } + } + } + } + }, + "/api/v3/config/naming": { + "get": { + "tags": [ + "NamingConfig" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + } + } + } + } + } + }, + "/api/v3/config/naming/{id}": { + "put": { + "tags": [ + "NamingConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "NamingConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NamingConfigResource" + } + } + } + } + } + } + }, + "/api/v3/config/naming/examples": { + "get": { + "tags": [ + "NamingConfig" + ], + "parameters": [ + { + "name": "RenameMovies", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "ReplaceIllegalCharacters", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "ColonReplacementFormat", + "in": "query", + "schema": { + "$ref": "#/components/schemas/ColonReplacementFormat" + } + }, + { + "name": "StandardMovieFormat", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "MovieFolderFormat", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "IncludeQuality", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "ReplaceSpaces", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "Separator", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "NumberStyle", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/notification": { + "get": { + "tags": [ + "Notification" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Notification" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + } + } + } + } + } + }, + "/api/v3/notification/{id}": { + "put": { + "tags": [ + "Notification" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Notification" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "Notification" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + } + } + } + } + } + }, + "/api/v3/notification/schema": { + "get": { + "tags": [ + "Notification" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationResource" + } + } + } + } + } + } + } + }, + "/api/v3/notification/test": { + "post": { + "tags": [ + "Notification" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/notification/testall": { + "post": { + "tags": [ + "Notification" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/notification/action/{name}": { + "post": { + "tags": [ + "Notification" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NotificationResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/parse": { + "get": { + "tags": [ + "Parse" + ], + "parameters": [ + { + "name": "title", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ParseResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ParseResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ParseResource" + } + } + } + } + } + } + }, + "/api/v3/qualitydefinition/{id}": { + "put": { + "tags": [ + "QualityDefinition" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "QualityDefinition" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + } + } + } + } + } + }, + "/api/v3/qualitydefinition": { + "get": { + "tags": [ + "QualityDefinition" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + } + } + } + } + } + } + }, + "/api/v3/qualitydefinition/update": { + "put": { + "tags": [ + "QualityDefinition" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityDefinitionResource" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/qualityprofile": { + "post": { + "tags": [ + "QualityProfile" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "QualityProfile" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityProfileResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityProfileResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityProfileResource" + } + } + } + } + } + } + } + }, + "/api/v3/qualityprofile/{id}": { + "delete": { + "tags": [ + "QualityProfile" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "put": { + "tags": [ + "QualityProfile" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "QualityProfile" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + } + } + } + } + } + }, + "/api/v3/qualityprofile/schema": { + "get": { + "tags": [ + "QualityProfileSchema" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QualityProfileResource" + } + } + } + } + } + } + }, + "/api/v3/queue/{id}": { + "delete": { + "tags": [ + "Queue" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "removeFromClient", + "in": "query", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "blocklist", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "Queue" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QueueResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QueueResource" + } + } + } + } + } + } + }, + "/api/v3/queue/bulk": { + "delete": { + "tags": [ + "Queue" + ], + "parameters": [ + { + "name": "removeFromClient", + "in": "query", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "blocklist", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueBulkResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QueueBulkResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/QueueBulkResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/queue": { + "get": { + "tags": [ + "Queue" + ], + "parameters": [ + { + "name": "includeUnknownMovieItems", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "includeMovie", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QueueResourcePagingResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueResourcePagingResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QueueResourcePagingResource" + } + } + } + } + } + } + }, + "/api/v3/queue/grab/{id}": { + "post": { + "tags": [ + "QueueAction" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/queue/grab/bulk": { + "post": { + "tags": [ + "QueueAction" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueBulkResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QueueBulkResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/QueueBulkResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/queue/details": { + "get": { + "tags": [ + "QueueDetails" + ], + "parameters": [ + { + "name": "movieId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "includeMovie", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueResource" + } + } + } + } + } + } + } + }, + "/api/v3/queue/details/{id}": { + "get": { + "tags": [ + "QueueDetails" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QueueResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QueueResource" + } + } + } + } + } + } + }, + "/api/v3/queue/status": { + "get": { + "tags": [ + "QueueStatus" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QueueStatusResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueStatusResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QueueStatusResource" + } + } + } + } + } + } + }, + "/api/v3/queue/status/{id}": { + "get": { + "tags": [ + "QueueStatus" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/QueueStatusResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueStatusResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/QueueStatusResource" + } + } + } + } + } + } + }, + "/api/v3/release": { + "post": { + "tags": [ + "Release" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "Release" + ], + "parameters": [ + { + "name": "movieId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReleaseResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReleaseResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReleaseResource" + } + } + } + } + } + } + } + }, + "/api/v3/release/{id}": { + "get": { + "tags": [ + "Release" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + } + } + } + } + } + }, + "/api/v3/release/push": { + "post": { + "tags": [ + "ReleasePush" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReleaseResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReleaseResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReleaseResource" + } + } + } + } + } + } + } + }, + "/api/v3/release/push/{id}": { + "get": { + "tags": [ + "ReleasePush" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseResource" + } + } + } + } + } + } + }, + "/api/v3/remotepathmapping": { + "post": { + "tags": [ + "RemotePathMapping" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "RemotePathMapping" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + } + } + } + } + } + } + }, + "/api/v3/remotepathmapping/{id}": { + "delete": { + "tags": [ + "RemotePathMapping" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "put": { + "tags": [ + "RemotePathMapping" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "RemotePathMapping" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemotePathMappingResource" + } + } + } + } + } + } + }, + "/api/v3/rename": { + "get": { + "tags": [ + "RenameMovie" + ], + "parameters": [ + { + "name": "movieId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RenameMovieResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RenameMovieResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RenameMovieResource" + } + } + } + } + } + } + } + }, + "/api/v3/restriction": { + "get": { + "tags": [ + "Restriction" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestrictionResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestrictionResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestrictionResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Restriction" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + } + } + } + } + } + }, + "/api/v3/restriction/{id}": { + "put": { + "tags": [ + "Restriction" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Restriction" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "Restriction" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RestrictionResource" + } + } + } + } + } + } + }, + "/api/v3/rootfolder": { + "post": { + "tags": [ + "RootFolder" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RootFolderResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RootFolderResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/RootFolderResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RootFolderResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RootFolderResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RootFolderResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "RootFolder" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RootFolderResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RootFolderResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RootFolderResource" + } + } + } + } + } + } + } + }, + "/api/v3/rootfolder/{id}": { + "delete": { + "tags": [ + "RootFolder" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "RootFolder" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RootFolderResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RootFolderResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RootFolderResource" + } + } + } + } + } + } + }, + "/content/{path}": { + "get": { + "tags": [ + "StaticResource" + ], + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "schema": { + "pattern": "^(?!api/).*", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/": { + "get": { + "tags": [ + "StaticResource" + ], + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/{path}": { + "get": { + "tags": [ + "StaticResource" + ], + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "schema": { + "pattern": "^(?!(api|feed)/).*", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/system/status": { + "get": { + "tags": [ + "System" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/system/routes": { + "get": { + "tags": [ + "System" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/system/routes/duplicate": { + "get": { + "tags": [ + "System" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/system/shutdown": { + "post": { + "tags": [ + "System" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/system/restart": { + "post": { + "tags": [ + "System" + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/v3/tag": { + "get": { + "tags": [ + "Tag" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Tag" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + } + } + } + } + } + }, + "/api/v3/tag/{id}": { + "put": { + "tags": [ + "Tag" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Tag" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "Tag" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/TagResource" + } + } + } + } + } + } + }, + "/api/v3/tag/detail": { + "get": { + "tags": [ + "TagDetails" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagDetailsResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagDetailsResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagDetailsResource" + } + } + } + } + } + } + } + }, + "/api/v3/tag/detail/{id}": { + "get": { + "tags": [ + "TagDetails" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/TagDetailsResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagDetailsResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/TagDetailsResource" + } + } + } + } + } + } + }, + "/api/v3/system/task": { + "get": { + "tags": [ + "Task" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskResource" + } + } + } + } + } + } + } + }, + "/api/v3/system/task/{id}": { + "get": { + "tags": [ + "Task" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/TaskResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/TaskResource" + } + } + } + } + } + } + }, + "/api/v3/config/ui": { + "get": { + "tags": [ + "UiConfig" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + } + } + } + } + } + }, + "/api/v3/config/ui/{id}": { + "put": { + "tags": [ + "UiConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "UiConfig" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UiConfigResource" + } + } + } + } + } + } + }, + "/api/v3/update": { + "get": { + "tags": [ + "Update" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UpdateResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UpdateResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UpdateResource" + } + } + } + } + } + } + } + }, + "/api/v3/log/file/update": { + "get": { + "tags": [ + "UpdateLogFile" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogFileResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogFileResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogFileResource" + } + } + } + } + } + } + } + }, + "/api/v3/log/file/update/{filename}": { + "get": { + "tags": [ + "UpdateLogFile" + ], + "parameters": [ + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "pattern": "[-.a-zA-Z0-9]+?\\.txt", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "AddMovieMethod": { + "enum": [ + "manual", + "list", + "collection" + ], + "type": "string" + }, + "AddMovieOptions": { + "type": "object", + "properties": { + "ignoreEpisodesWithFiles": { + "type": "boolean" + }, + "ignoreEpisodesWithoutFiles": { + "type": "boolean" + }, + "monitor": { + "$ref": "#/components/schemas/MonitorTypes" + }, + "searchForMovie": { + "type": "boolean" + }, + "addMethod": { + "$ref": "#/components/schemas/AddMovieMethod" + } + }, + "additionalProperties": false + }, + "AlternativeTitle": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "sourceType": { + "$ref": "#/components/schemas/SourceType" + }, + "movieMetadataId": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "cleanTitle": { + "type": "string", + "nullable": true + }, + "sourceId": { + "type": "integer", + "format": "int32" + }, + "votes": { + "type": "integer", + "format": "int32" + }, + "voteCount": { + "type": "integer", + "format": "int32" + }, + "language": { + "$ref": "#/components/schemas/Language" + } + }, + "additionalProperties": false + }, + "AlternativeTitleResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "sourceType": { + "$ref": "#/components/schemas/SourceType" + }, + "movieMetadataId": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "cleanTitle": { + "type": "string", + "nullable": true + }, + "sourceId": { + "type": "integer", + "format": "int32" + }, + "votes": { + "type": "integer", + "format": "int32" + }, + "voteCount": { + "type": "integer", + "format": "int32" + }, + "language": { + "$ref": "#/components/schemas/Language" + } + }, + "additionalProperties": false + }, + "ApplyTags": { + "enum": [ + "add", + "remove", + "replace" + ], + "type": "string" + }, + "AuthenticationType": { + "enum": [ + "none", + "basic", + "forms" + ], + "type": "string" + }, + "BackupResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "path": { + "type": "string", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/BackupType" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "time": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, + "BackupType": { + "enum": [ + "scheduled", + "manual", + "update" + ], + "type": "string" + }, + "BlocklistBulkResource": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "BlocklistResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "movieId": { + "type": "integer", + "format": "int32" + }, + "sourceTitle": { + "type": "string", + "nullable": true + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Language" + }, + "nullable": true + }, + "quality": { + "$ref": "#/components/schemas/QualityModel" + }, + "customFormats": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatResource" + }, + "nullable": true + }, + "date": { + "type": "string", + "format": "date-time" + }, + "protocol": { + "$ref": "#/components/schemas/DownloadProtocol" + }, + "indexer": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + }, + "movie": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "additionalProperties": false + }, + "BlocklistResourcePagingResource": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "format": "int32" + }, + "sortKey": { + "type": "string", + "nullable": true + }, + "sortDirection": { + "$ref": "#/components/schemas/SortDirection" + }, + "filters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PagingResourceFilter" + }, + "nullable": true + }, + "totalRecords": { + "type": "integer", + "format": "int32" + }, + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlocklistResource" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "CertificateValidationType": { + "enum": [ + "enabled", + "disabledForLocalAddresses", + "disabled" + ], + "type": "string" + }, + "CollectionMovieResource": { + "type": "object", + "properties": { + "tmdbId": { + "type": "integer", + "format": "int32" + }, + "imdbId": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "cleanTitle": { + "type": "string", + "nullable": true + }, + "sortTitle": { + "type": "string", + "nullable": true + }, + "overview": { + "type": "string", + "nullable": true + }, + "runtime": { + "type": "integer", + "format": "int32" + }, + "images": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaCover" + }, + "nullable": true + }, + "year": { + "type": "integer", + "format": "int32" + }, + "ratings": { + "$ref": "#/components/schemas/Ratings" + }, + "genres": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "folder": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "CollectionResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "sortTitle": { + "type": "string", + "nullable": true + }, + "tmdbId": { + "type": "integer", + "format": "int32" + }, + "images": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaCover" + }, + "nullable": true + }, + "overview": { + "type": "string", + "nullable": true + }, + "monitored": { + "type": "boolean" + }, + "rootFolderPath": { + "type": "string", + "nullable": true + }, + "qualityProfileId": { + "type": "integer", + "format": "int32" + }, + "searchOnAdd": { + "type": "boolean" + }, + "minimumAvailability": { + "$ref": "#/components/schemas/MovieStatusType" + }, + "movies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CollectionMovieResource" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "CollectionUpdateResource": { + "type": "object", + "properties": { + "collectionIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "monitored": { + "type": "boolean", + "nullable": true + }, + "monitorMovies": { + "type": "boolean", + "nullable": true + }, + "qualityProfileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "rootFolderPath": { + "type": "string", + "nullable": true + }, + "minimumAvailability": { + "$ref": "#/components/schemas/MovieStatusType" + } + }, + "additionalProperties": false + }, + "ColonReplacementFormat": { + "enum": [ + "delete", + "dash", + "spaceDash", + "spaceDashSpace" + ], + "type": "string" + }, + "Command": { + "type": "object", + "properties": { + "sendUpdatesToClient": { + "type": "boolean" + }, + "updateScheduledTask": { + "type": "boolean", + "readOnly": true + }, + "completionMessage": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "requiresDiskAccess": { + "type": "boolean", + "readOnly": true + }, + "isExclusive": { + "type": "boolean", + "readOnly": true + }, + "isTypeExclusive": { + "type": "boolean", + "readOnly": true + }, + "name": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "lastExecutionTime": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "lastStartTime": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "trigger": { + "$ref": "#/components/schemas/CommandTrigger" + }, + "suppressMessages": { + "type": "boolean" + }, + "clientUserAgent": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "CommandPriority": { + "enum": [ + "normal", + "high", + "low" + ], + "type": "string" + }, + "CommandResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "commandName": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + }, + "body": { + "$ref": "#/components/schemas/Command" + }, + "priority": { + "$ref": "#/components/schemas/CommandPriority" + }, + "status": { + "$ref": "#/components/schemas/CommandStatus" + }, + "queued": { + "type": "string", + "format": "date-time" + }, + "started": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "ended": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "duration": { + "$ref": "#/components/schemas/TimeSpan" + }, + "exception": { + "type": "string", + "nullable": true + }, + "trigger": { + "$ref": "#/components/schemas/CommandTrigger" + }, + "clientUserAgent": { + "type": "string", + "nullable": true + }, + "stateChangeTime": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "sendUpdatesToClient": { + "type": "boolean" + }, + "updateScheduledTask": { + "type": "boolean" + }, + "lastExecutionTime": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false + }, + "CommandStatus": { + "enum": [ + "queued", + "started", + "completed", + "failed", + "aborted", + "cancelled", + "orphaned" + ], + "type": "string" + }, + "CommandTrigger": { + "enum": [ + "unspecified", + "manual", + "scheduled" + ], + "type": "string" + }, + "CreditResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "personName": { + "type": "string", + "nullable": true + }, + "creditTmdbId": { + "type": "string", + "nullable": true + }, + "personTmdbId": { + "type": "integer", + "format": "int32" + }, + "movieMetadataId": { + "type": "integer", + "format": "int32" + }, + "images": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaCover" + }, + "nullable": true + }, + "department": { + "type": "string", + "nullable": true + }, + "job": { + "type": "string", + "nullable": true + }, + "character": { + "type": "string", + "nullable": true + }, + "order": { + "type": "integer", + "format": "int32" + }, + "type": { + "$ref": "#/components/schemas/CreditType" + } + }, + "additionalProperties": false + }, + "CreditType": { + "enum": [ + "cast", + "crew" + ], + "type": "string" + }, + "CustomFilterResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "nullable": true + }, + "label": { + "type": "string", + "nullable": true + }, + "filters": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { } + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "CustomFormatResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "includeCustomFormatWhenRenaming": { + "type": "boolean" + }, + "specifications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatSpecificationSchema" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "CustomFormatSpecificationSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "implementation": { + "type": "string", + "nullable": true + }, + "implementationName": { + "type": "string", + "nullable": true + }, + "infoLink": { + "type": "string", + "nullable": true + }, + "negate": { + "type": "boolean" + }, + "required": { + "type": "boolean" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + }, + "nullable": true + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatSpecificationSchema" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "DelayProfileResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "enableUsenet": { + "type": "boolean" + }, + "enableTorrent": { + "type": "boolean" + }, + "preferredProtocol": { + "$ref": "#/components/schemas/DownloadProtocol" + }, + "usenetDelay": { + "type": "integer", + "format": "int32" + }, + "torrentDelay": { + "type": "integer", + "format": "int32" + }, + "bypassIfHighestQuality": { + "type": "boolean" + }, + "order": { + "type": "integer", + "format": "int32" + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "DiskSpaceResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "path": { + "type": "string", + "nullable": true + }, + "label": { + "type": "string", + "nullable": true + }, + "freeSpace": { + "type": "integer", + "format": "int64" + }, + "totalSpace": { + "type": "integer", + "format": "int64" + } + }, + "additionalProperties": false + }, + "DownloadClientConfigResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "downloadClientWorkingFolders": { + "type": "string", + "nullable": true + }, + "enableCompletedDownloadHandling": { + "type": "boolean" + }, + "checkForFinishedDownloadInterval": { + "type": "integer", + "format": "int32" + }, + "autoRedownloadFailed": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "DownloadClientResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + }, + "nullable": true + }, + "implementationName": { + "type": "string", + "nullable": true + }, + "implementation": { + "type": "string", + "nullable": true + }, + "configContract": { + "type": "string", + "nullable": true + }, + "infoLink": { + "type": "string", + "nullable": true + }, + "message": { + "$ref": "#/components/schemas/ProviderMessage" + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DownloadClientResource" + }, + "nullable": true + }, + "enable": { + "type": "boolean" + }, + "protocol": { + "$ref": "#/components/schemas/DownloadProtocol" + }, + "priority": { + "type": "integer", + "format": "int32" + }, + "removeCompletedDownloads": { + "type": "boolean" + }, + "removeFailedDownloads": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "DownloadProtocol": { + "enum": [ + "unknown", + "usenet", + "torrent" + ], + "type": "string" + }, + "ExtraFileResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "movieId": { + "type": "integer", + "format": "int32" + }, + "movieFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "relativePath": { + "type": "string", + "nullable": true + }, + "extension": { + "type": "string", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/ExtraFileType" + } + }, + "additionalProperties": false + }, + "ExtraFileType": { + "enum": [ + "subtitle", + "metadata", + "other" + ], + "type": "string" + }, + "Field": { + "type": "object", + "properties": { + "order": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "label": { + "type": "string", + "nullable": true + }, + "unit": { + "type": "string", + "nullable": true + }, + "helpText": { + "type": "string", + "nullable": true + }, + "helpLink": { + "type": "string", + "nullable": true + }, + "value": { + "nullable": true + }, + "type": { + "type": "string", + "nullable": true + }, + "advanced": { + "type": "boolean" + }, + "selectOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SelectOption" + }, + "nullable": true + }, + "selectOptionsProviderAction": { + "type": "string", + "nullable": true + }, + "section": { + "type": "string", + "nullable": true + }, + "hidden": { + "type": "string", + "nullable": true + }, + "placeholder": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "FileDateType": { + "enum": [ + "none", + "cinemas", + "release" + ], + "type": "string" + }, + "HealthCheckResult": { + "enum": [ + "ok", + "notice", + "warning", + "error" + ], + "type": "string" + }, + "HealthResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "source": { + "type": "string", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/HealthCheckResult" + }, + "message": { + "type": "string", + "nullable": true + }, + "wikiUrl": { + "$ref": "#/components/schemas/HttpUri" + } + }, + "additionalProperties": false + }, + "HistoryResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "movieId": { + "type": "integer", + "format": "int32" + }, + "sourceTitle": { + "type": "string", + "nullable": true + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Language" + }, + "nullable": true + }, + "quality": { + "$ref": "#/components/schemas/QualityModel" + }, + "customFormats": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatResource" + }, + "nullable": true + }, + "qualityCutoffNotMet": { + "type": "boolean" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "downloadId": { + "type": "string", + "nullable": true + }, + "eventType": { + "$ref": "#/components/schemas/MovieHistoryEventType" + }, + "data": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "movie": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "additionalProperties": false + }, + "HistoryResourcePagingResource": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "format": "int32" + }, + "sortKey": { + "type": "string", + "nullable": true + }, + "sortDirection": { + "$ref": "#/components/schemas/SortDirection" + }, + "filters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PagingResourceFilter" + }, + "nullable": true + }, + "totalRecords": { + "type": "integer", + "format": "int32" + }, + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HistoryResource" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "HostConfigResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "bindAddress": { + "type": "string", + "nullable": true + }, + "port": { + "type": "integer", + "format": "int32" + }, + "sslPort": { + "type": "integer", + "format": "int32" + }, + "enableSsl": { + "type": "boolean" + }, + "launchBrowser": { + "type": "boolean" + }, + "authenticationMethod": { + "$ref": "#/components/schemas/AuthenticationType" + }, + "analyticsEnabled": { + "type": "boolean" + }, + "username": { + "type": "string", + "nullable": true + }, + "password": { + "type": "string", + "nullable": true + }, + "logLevel": { + "type": "string", + "nullable": true + }, + "consoleLogLevel": { + "type": "string", + "nullable": true + }, + "branch": { + "type": "string", + "nullable": true + }, + "apiKey": { + "type": "string", + "nullable": true + }, + "sslCertPath": { + "type": "string", + "nullable": true + }, + "sslCertPassword": { + "type": "string", + "nullable": true + }, + "urlBase": { + "type": "string", + "nullable": true + }, + "instanceName": { + "type": "string", + "nullable": true + }, + "applicationUrl": { + "type": "string", + "nullable": true + }, + "updateAutomatically": { + "type": "boolean" + }, + "updateMechanism": { + "$ref": "#/components/schemas/UpdateMechanism" + }, + "updateScriptPath": { + "type": "string", + "nullable": true + }, + "proxyEnabled": { + "type": "boolean" + }, + "proxyType": { + "$ref": "#/components/schemas/ProxyType" + }, + "proxyHostname": { + "type": "string", + "nullable": true + }, + "proxyPort": { + "type": "integer", + "format": "int32" + }, + "proxyUsername": { + "type": "string", + "nullable": true + }, + "proxyPassword": { + "type": "string", + "nullable": true + }, + "proxyBypassFilter": { + "type": "string", + "nullable": true + }, + "proxyBypassLocalAddresses": { + "type": "boolean" + }, + "certificateValidation": { + "$ref": "#/components/schemas/CertificateValidationType" + }, + "backupFolder": { + "type": "string", + "nullable": true + }, + "backupInterval": { + "type": "integer", + "format": "int32" + }, + "backupRetention": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "HttpUri": { + "type": "object", + "properties": { + "fullUri": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "scheme": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "host": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "port": { + "type": "integer", + "format": "int32", + "nullable": true, + "readOnly": true + }, + "path": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "query": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "fragment": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, + "ImportExclusionsResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + }, + "nullable": true + }, + "implementationName": { + "type": "string", + "nullable": true + }, + "implementation": { + "type": "string", + "nullable": true + }, + "configContract": { + "type": "string", + "nullable": true + }, + "infoLink": { + "type": "string", + "nullable": true + }, + "message": { + "$ref": "#/components/schemas/ProviderMessage" + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportExclusionsResource" + }, + "nullable": true + }, + "tmdbId": { + "type": "integer", + "format": "int32" + }, + "movieTitle": { + "type": "string", + "nullable": true + }, + "movieYear": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "ImportListConfigResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "importListSyncInterval": { + "type": "integer", + "format": "int32" + }, + "listSyncLevel": { + "type": "string", + "nullable": true + }, + "importExclusions": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "ImportListResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + }, + "nullable": true + }, + "implementationName": { + "type": "string", + "nullable": true + }, + "implementation": { + "type": "string", + "nullable": true + }, + "configContract": { + "type": "string", + "nullable": true + }, + "infoLink": { + "type": "string", + "nullable": true + }, + "message": { + "$ref": "#/components/schemas/ProviderMessage" + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportListResource" + }, + "nullable": true + }, + "enabled": { + "type": "boolean" + }, + "enableAuto": { + "type": "boolean" + }, + "monitor": { + "$ref": "#/components/schemas/MonitorTypes" + }, + "rootFolderPath": { + "type": "string", + "nullable": true + }, + "qualityProfileId": { + "type": "integer", + "format": "int32" + }, + "searchOnAdd": { + "type": "boolean" + }, + "minimumAvailability": { + "$ref": "#/components/schemas/MovieStatusType" + }, + "listType": { + "$ref": "#/components/schemas/ImportListType" + }, + "listOrder": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "ImportListType": { + "enum": [ + "program", + "tmdb", + "trakt", + "plex", + "other", + "advanced" + ], + "type": "string" + }, + "IndexerConfigResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "minimumAge": { + "type": "integer", + "format": "int32" + }, + "maximumSize": { + "type": "integer", + "format": "int32" + }, + "retention": { + "type": "integer", + "format": "int32" + }, + "rssSyncInterval": { + "type": "integer", + "format": "int32" + }, + "preferIndexerFlags": { + "type": "boolean" + }, + "availabilityDelay": { + "type": "integer", + "format": "int32" + }, + "allowHardcodedSubs": { + "type": "boolean" + }, + "whitelistedHardcodedSubs": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "IndexerFlagResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "nameLower": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, + "IndexerResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + }, + "nullable": true + }, + "implementationName": { + "type": "string", + "nullable": true + }, + "implementation": { + "type": "string", + "nullable": true + }, + "configContract": { + "type": "string", + "nullable": true + }, + "infoLink": { + "type": "string", + "nullable": true + }, + "message": { + "$ref": "#/components/schemas/ProviderMessage" + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerResource" + }, + "nullable": true + }, + "enableRss": { + "type": "boolean" + }, + "enableAutomaticSearch": { + "type": "boolean" + }, + "enableInteractiveSearch": { + "type": "boolean" + }, + "supportsRss": { + "type": "boolean" + }, + "supportsSearch": { + "type": "boolean" + }, + "protocol": { + "$ref": "#/components/schemas/DownloadProtocol" + }, + "priority": { + "type": "integer", + "format": "int32" + }, + "downloadClientId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "Language": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LanguageResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "nameLower": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, + "LogFileResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "filename": { + "type": "string", + "nullable": true + }, + "lastWriteTime": { + "type": "string", + "format": "date-time" + }, + "contentsUrl": { + "type": "string", + "nullable": true + }, + "downloadUrl": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LogResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "exception": { + "type": "string", + "nullable": true + }, + "exceptionType": { + "type": "string", + "nullable": true + }, + "level": { + "type": "string", + "nullable": true + }, + "logger": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + }, + "method": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LogResourcePagingResource": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "format": "int32" + }, + "sortKey": { + "type": "string", + "nullable": true + }, + "sortDirection": { + "$ref": "#/components/schemas/SortDirection" + }, + "filters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PagingResourceFilter" + }, + "nullable": true + }, + "totalRecords": { + "type": "integer", + "format": "int32" + }, + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogResource" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "ManualImportReprocessResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "path": { + "type": "string", + "nullable": true + }, + "movieId": { + "type": "integer", + "format": "int32" + }, + "movie": { + "$ref": "#/components/schemas/MovieResource" + }, + "quality": { + "$ref": "#/components/schemas/QualityModel" + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Language" + }, + "nullable": true + }, + "releaseGroup": { + "type": "string", + "nullable": true + }, + "downloadId": { + "type": "string", + "nullable": true + }, + "rejections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Rejection" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "ManualImportResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "path": { + "type": "string", + "nullable": true + }, + "relativePath": { + "type": "string", + "nullable": true + }, + "folderName": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "size": { + "type": "integer", + "format": "int64" + }, + "movie": { + "$ref": "#/components/schemas/MovieResource" + }, + "quality": { + "$ref": "#/components/schemas/QualityModel" + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Language" + }, + "nullable": true + }, + "releaseGroup": { + "type": "string", + "nullable": true + }, + "qualityWeight": { + "type": "integer", + "format": "int32" + }, + "downloadId": { + "type": "string", + "nullable": true + }, + "rejections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Rejection" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "MediaCover": { + "type": "object", + "properties": { + "coverType": { + "$ref": "#/components/schemas/MediaCoverTypes" + }, + "url": { + "type": "string", + "nullable": true + }, + "remoteUrl": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "MediaCoverTypes": { + "enum": [ + "unknown", + "poster", + "banner", + "fanart", + "screenshot", + "headshot" + ], + "type": "string" + }, + "MediaInfoResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "audioBitrate": { + "type": "integer", + "format": "int64" + }, + "audioChannels": { + "type": "number", + "format": "double" + }, + "audioCodec": { + "type": "string", + "nullable": true + }, + "audioLanguages": { + "type": "string", + "nullable": true + }, + "audioStreamCount": { + "type": "integer", + "format": "int32" + }, + "videoBitDepth": { + "type": "integer", + "format": "int32" + }, + "videoBitrate": { + "type": "integer", + "format": "int64" + }, + "videoCodec": { + "type": "string", + "nullable": true + }, + "videoDynamicRangeType": { + "type": "string", + "nullable": true + }, + "videoFps": { + "type": "number", + "format": "double" + }, + "resolution": { + "type": "string", + "nullable": true + }, + "runTime": { + "type": "string", + "nullable": true + }, + "scanType": { + "type": "string", + "nullable": true + }, + "subtitles": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "MediaManagementConfigResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "autoUnmonitorPreviouslyDownloadedMovies": { + "type": "boolean" + }, + "recycleBin": { + "type": "string", + "nullable": true + }, + "recycleBinCleanupDays": { + "type": "integer", + "format": "int32" + }, + "downloadPropersAndRepacks": { + "$ref": "#/components/schemas/ProperDownloadTypes" + }, + "createEmptyMovieFolders": { + "type": "boolean" + }, + "deleteEmptyFolders": { + "type": "boolean" + }, + "fileDate": { + "$ref": "#/components/schemas/FileDateType" + }, + "rescanAfterRefresh": { + "$ref": "#/components/schemas/RescanAfterRefreshType" + }, + "autoRenameFolders": { + "type": "boolean" + }, + "pathsDefaultStatic": { + "type": "boolean" + }, + "setPermissionsLinux": { + "type": "boolean" + }, + "chmodFolder": { + "type": "string", + "nullable": true + }, + "chownGroup": { + "type": "string", + "nullable": true + }, + "skipFreeSpaceCheckWhenImporting": { + "type": "boolean" + }, + "minimumFreeSpaceWhenImporting": { + "type": "integer", + "format": "int32" + }, + "copyUsingHardlinks": { + "type": "boolean" + }, + "importExtraFiles": { + "type": "boolean" + }, + "extraFileExtensions": { + "type": "string", + "nullable": true + }, + "enableMediaInfo": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "MetadataConfigResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "certificationCountry": { + "$ref": "#/components/schemas/TMDbCountryCode" + } + }, + "additionalProperties": false + }, + "MetadataResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + }, + "nullable": true + }, + "implementationName": { + "type": "string", + "nullable": true + }, + "implementation": { + "type": "string", + "nullable": true + }, + "configContract": { + "type": "string", + "nullable": true + }, + "infoLink": { + "type": "string", + "nullable": true + }, + "message": { + "$ref": "#/components/schemas/ProviderMessage" + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataResource" + }, + "nullable": true + }, + "enable": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Modifier": { + "enum": [ + "none", + "regional", + "screener", + "rawhd", + "brdisk", + "remux" + ], + "type": "string" + }, + "MonitorTypes": { + "enum": [ + "movieOnly", + "movieAndCollection", + "none" + ], + "type": "string" + }, + "MovieCollection": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "cleanTitle": { + "type": "string", + "nullable": true + }, + "sortTitle": { + "type": "string", + "nullable": true + }, + "tmdbId": { + "type": "integer", + "format": "int32" + }, + "overview": { + "type": "string", + "nullable": true + }, + "monitored": { + "type": "boolean" + }, + "qualityProfileId": { + "type": "integer", + "format": "int32" + }, + "rootFolderPath": { + "type": "string", + "nullable": true + }, + "searchOnAdd": { + "type": "boolean" + }, + "minimumAvailability": { + "$ref": "#/components/schemas/MovieStatusType" + }, + "lastInfoSync": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "images": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaCover" + }, + "nullable": true + }, + "added": { + "type": "string", + "format": "date-time" + }, + "movies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieMetadata" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "MovieEditorResource": { + "type": "object", + "properties": { + "movieIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "monitored": { + "type": "boolean", + "nullable": true + }, + "qualityProfileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "minimumAvailability": { + "$ref": "#/components/schemas/MovieStatusType" + }, + "rootFolderPath": { + "type": "string", + "nullable": true + }, + "tags": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "applyTags": { + "$ref": "#/components/schemas/ApplyTags" + }, + "moveFiles": { + "type": "boolean" + }, + "deleteFiles": { + "type": "boolean" + }, + "addImportExclusion": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "MovieFileListResource": { + "type": "object", + "properties": { + "movieFileIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Language" + }, + "nullable": true + }, + "quality": { + "$ref": "#/components/schemas/QualityModel" + }, + "edition": { + "type": "string", + "nullable": true + }, + "releaseGroup": { + "type": "string", + "nullable": true + }, + "sceneName": { + "type": "string", + "nullable": true + }, + "indexerFlags": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "MovieFileResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "movieId": { + "type": "integer", + "format": "int32" + }, + "relativePath": { + "type": "string", + "nullable": true + }, + "path": { + "type": "string", + "nullable": true + }, + "size": { + "type": "integer", + "format": "int64" + }, + "dateAdded": { + "type": "string", + "format": "date-time" + }, + "sceneName": { + "type": "string", + "nullable": true + }, + "indexerFlags": { + "type": "integer", + "format": "int32" + }, + "quality": { + "$ref": "#/components/schemas/QualityModel" + }, + "customFormats": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatResource" + }, + "nullable": true + }, + "mediaInfo": { + "$ref": "#/components/schemas/MediaInfoResource" + }, + "originalFilePath": { + "type": "string", + "nullable": true + }, + "qualityCutoffNotMet": { + "type": "boolean" + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Language" + }, + "nullable": true + }, + "releaseGroup": { + "type": "string", + "nullable": true + }, + "edition": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "MovieHistoryEventType": { + "enum": [ + "unknown", + "grabbed", + "downloadFolderImported", + "downloadFailed", + "movieFileDeleted", + "movieFolderImported", + "movieFileRenamed", + "downloadIgnored" + ], + "type": "string" + }, + "MovieMetadata": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "tmdbId": { + "type": "integer", + "format": "int32" + }, + "images": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaCover" + }, + "nullable": true + }, + "genres": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "inCinemas": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "physicalRelease": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "digitalRelease": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "certification": { + "type": "string", + "nullable": true + }, + "year": { + "type": "integer", + "format": "int32" + }, + "ratings": { + "$ref": "#/components/schemas/Ratings" + }, + "collectionTmdbId": { + "type": "integer", + "format": "int32" + }, + "collectionTitle": { + "type": "string", + "nullable": true + }, + "lastInfoSync": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "runtime": { + "type": "integer", + "format": "int32" + }, + "website": { + "type": "string", + "nullable": true + }, + "imdbId": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "cleanTitle": { + "type": "string", + "nullable": true + }, + "sortTitle": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/MovieStatusType" + }, + "overview": { + "type": "string", + "nullable": true + }, + "alternativeTitles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlternativeTitle" + }, + "nullable": true + }, + "translations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MovieTranslation" + }, + "nullable": true + }, + "secondaryYear": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "youTubeTrailerId": { + "type": "string", + "nullable": true + }, + "studio": { + "type": "string", + "nullable": true + }, + "originalTitle": { + "type": "string", + "nullable": true + }, + "cleanOriginalTitle": { + "type": "string", + "nullable": true + }, + "originalLanguage": { + "$ref": "#/components/schemas/Language" + }, + "recommendations": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "popularity": { + "type": "number", + "format": "float" + }, + "isRecentMovie": { + "type": "boolean", + "readOnly": true + } + }, + "additionalProperties": false + }, + "MovieResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "originalTitle": { + "type": "string", + "nullable": true + }, + "originalLanguage": { + "$ref": "#/components/schemas/Language" + }, + "alternateTitles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlternativeTitleResource" + }, + "nullable": true + }, + "secondaryYear": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "secondaryYearSourceId": { + "type": "integer", + "format": "int32" + }, + "sortTitle": { + "type": "string", + "nullable": true + }, + "sizeOnDisk": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/MovieStatusType" + }, + "overview": { + "type": "string", + "nullable": true + }, + "inCinemas": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "physicalRelease": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "digitalRelease": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "physicalReleaseNote": { + "type": "string", + "nullable": true + }, + "images": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaCover" + }, + "nullable": true + }, + "website": { + "type": "string", + "nullable": true + }, + "remotePoster": { + "type": "string", + "nullable": true + }, + "year": { + "type": "integer", + "format": "int32" + }, + "hasFile": { + "type": "boolean" + }, + "youTubeTrailerId": { + "type": "string", + "nullable": true + }, + "studio": { + "type": "string", + "nullable": true + }, + "path": { + "type": "string", + "nullable": true + }, + "qualityProfileId": { + "type": "integer", + "format": "int32" + }, + "monitored": { + "type": "boolean" + }, + "minimumAvailability": { + "$ref": "#/components/schemas/MovieStatusType" + }, + "isAvailable": { + "type": "boolean" + }, + "folderName": { + "type": "string", + "nullable": true + }, + "runtime": { + "type": "integer", + "format": "int32" + }, + "cleanTitle": { + "type": "string", + "nullable": true + }, + "imdbId": { + "type": "string", + "nullable": true + }, + "tmdbId": { + "type": "integer", + "format": "int32" + }, + "titleSlug": { + "type": "string", + "nullable": true + }, + "rootFolderPath": { + "type": "string", + "nullable": true + }, + "folder": { + "type": "string", + "nullable": true + }, + "certification": { + "type": "string", + "nullable": true + }, + "genres": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "added": { + "type": "string", + "format": "date-time" + }, + "addOptions": { + "$ref": "#/components/schemas/AddMovieOptions" + }, + "ratings": { + "$ref": "#/components/schemas/Ratings" + }, + "movieFile": { + "$ref": "#/components/schemas/MovieFileResource" + }, + "collection": { + "$ref": "#/components/schemas/MovieCollection" + }, + "popularity": { + "type": "number", + "format": "float" + } + }, + "additionalProperties": false + }, + "MovieRuntimeFormatType": { + "enum": [ + "hoursMinutes", + "minutes" + ], + "type": "string" + }, + "MovieStatusType": { + "enum": [ + "tba", + "announced", + "inCinemas", + "released", + "deleted" + ], + "type": "string" + }, + "MovieTranslation": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "movieMetadataId": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "cleanTitle": { + "type": "string", + "nullable": true + }, + "overview": { + "type": "string", + "nullable": true + }, + "language": { + "$ref": "#/components/schemas/Language" + } + }, + "additionalProperties": false + }, + "NamingConfigResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "renameMovies": { + "type": "boolean" + }, + "replaceIllegalCharacters": { + "type": "boolean" + }, + "colonReplacementFormat": { + "$ref": "#/components/schemas/ColonReplacementFormat" + }, + "standardMovieFormat": { + "type": "string", + "nullable": true + }, + "movieFolderFormat": { + "type": "string", + "nullable": true + }, + "includeQuality": { + "type": "boolean" + }, + "replaceSpaces": { + "type": "boolean" + }, + "separator": { + "type": "string", + "nullable": true + }, + "numberStyle": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "NotificationResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + }, + "nullable": true + }, + "implementationName": { + "type": "string", + "nullable": true + }, + "implementation": { + "type": "string", + "nullable": true + }, + "configContract": { + "type": "string", + "nullable": true + }, + "infoLink": { + "type": "string", + "nullable": true + }, + "message": { + "$ref": "#/components/schemas/ProviderMessage" + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationResource" + }, + "nullable": true + }, + "link": { + "type": "string", + "nullable": true + }, + "onGrab": { + "type": "boolean" + }, + "onDownload": { + "type": "boolean" + }, + "onUpgrade": { + "type": "boolean" + }, + "onRename": { + "type": "boolean" + }, + "onMovieAdded": { + "type": "boolean" + }, + "onMovieDelete": { + "type": "boolean" + }, + "onMovieFileDelete": { + "type": "boolean" + }, + "onMovieFileDeleteForUpgrade": { + "type": "boolean" + }, + "onHealthIssue": { + "type": "boolean" + }, + "onApplicationUpdate": { + "type": "boolean" + }, + "supportsOnGrab": { + "type": "boolean" + }, + "supportsOnDownload": { + "type": "boolean" + }, + "supportsOnUpgrade": { + "type": "boolean" + }, + "supportsOnRename": { + "type": "boolean" + }, + "supportsOnMovieAdded": { + "type": "boolean" + }, + "supportsOnMovieDelete": { + "type": "boolean" + }, + "supportsOnMovieFileDelete": { + "type": "boolean" + }, + "supportsOnMovieFileDeleteForUpgrade": { + "type": "boolean" + }, + "supportsOnHealthIssue": { + "type": "boolean" + }, + "supportsOnApplicationUpdate": { + "type": "boolean" + }, + "includeHealthWarnings": { + "type": "boolean" + }, + "testCommand": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "PagingResourceFilter": { + "type": "object", + "properties": { + "key": { + "type": "string", + "nullable": true + }, + "value": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "ParseResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "parsedMovieInfo": { + "$ref": "#/components/schemas/ParsedMovieInfo" + }, + "movie": { + "$ref": "#/components/schemas/MovieResource" + } + }, + "additionalProperties": false + }, + "ParsedMovieInfo": { + "type": "object", + "properties": { + "movieTitles": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "originalTitle": { + "type": "string", + "nullable": true + }, + "releaseTitle": { + "type": "string", + "nullable": true + }, + "simpleReleaseTitle": { + "type": "string", + "nullable": true + }, + "quality": { + "$ref": "#/components/schemas/QualityModel" + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Language" + }, + "nullable": true + }, + "releaseGroup": { + "type": "string", + "nullable": true + }, + "releaseHash": { + "type": "string", + "nullable": true + }, + "edition": { + "type": "string", + "nullable": true + }, + "year": { + "type": "integer", + "format": "int32" + }, + "imdbId": { + "type": "string", + "nullable": true + }, + "tmdbId": { + "type": "integer", + "format": "int32" + }, + "extraInfo": { + "type": "object", + "additionalProperties": { + "nullable": true + }, + "nullable": true + }, + "movieTitle": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "primaryMovieTitle": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, + "ProfileFormatItemResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "format": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "score": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "ProperDownloadTypes": { + "enum": [ + "preferAndUpgrade", + "doNotUpgrade", + "doNotPrefer" + ], + "type": "string" + }, + "ProviderMessage": { + "type": "object", + "properties": { + "message": { + "type": "string", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/ProviderMessageType" + } + }, + "additionalProperties": false + }, + "ProviderMessageType": { + "enum": [ + "info", + "warning", + "error" + ], + "type": "string" + }, + "ProxyType": { + "enum": [ + "http", + "socks4", + "socks5" + ], + "type": "string" + }, + "Quality": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "source": { + "$ref": "#/components/schemas/Source" + }, + "resolution": { + "type": "integer", + "format": "int32" + }, + "modifier": { + "$ref": "#/components/schemas/Modifier" + } + }, + "additionalProperties": false + }, + "QualityDefinitionResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "quality": { + "$ref": "#/components/schemas/Quality" + }, + "title": { + "type": "string", + "nullable": true + }, + "weight": { + "type": "integer", + "format": "int32" + }, + "minSize": { + "type": "number", + "format": "double", + "nullable": true + }, + "maxSize": { + "type": "number", + "format": "double", + "nullable": true + }, + "preferredSize": { + "type": "number", + "format": "double", + "nullable": true + } + }, + "additionalProperties": false + }, + "QualityModel": { + "type": "object", + "properties": { + "quality": { + "$ref": "#/components/schemas/Quality" + }, + "revision": { + "$ref": "#/components/schemas/Revision" + }, + "hardcodedSubs": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "QualityProfileQualityItemResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "quality": { + "$ref": "#/components/schemas/Quality" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityProfileQualityItemResource" + }, + "nullable": true + }, + "allowed": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "QualityProfileResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "upgradeAllowed": { + "type": "boolean" + }, + "cutoff": { + "type": "integer", + "format": "int32" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QualityProfileQualityItemResource" + }, + "nullable": true + }, + "minFormatScore": { + "type": "integer", + "format": "int32" + }, + "cutoffFormatScore": { + "type": "integer", + "format": "int32" + }, + "formatItems": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProfileFormatItemResource" + }, + "nullable": true + }, + "language": { + "$ref": "#/components/schemas/Language" + } + }, + "additionalProperties": false + }, + "QueueBulkResource": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "QueueResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "movieId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "movie": { + "$ref": "#/components/schemas/MovieResource" + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Language" + }, + "nullable": true + }, + "quality": { + "$ref": "#/components/schemas/QualityModel" + }, + "customFormats": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatResource" + }, + "nullable": true + }, + "size": { + "type": "number", + "format": "double" + }, + "title": { + "type": "string", + "nullable": true + }, + "sizeleft": { + "type": "number", + "format": "double" + }, + "timeleft": { + "$ref": "#/components/schemas/TimeSpan" + }, + "estimatedCompletionTime": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "status": { + "type": "string", + "nullable": true + }, + "trackedDownloadStatus": { + "$ref": "#/components/schemas/TrackedDownloadStatus" + }, + "trackedDownloadState": { + "$ref": "#/components/schemas/TrackedDownloadState" + }, + "statusMessages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TrackedDownloadStatusMessage" + }, + "nullable": true + }, + "errorMessage": { + "type": "string", + "nullable": true + }, + "downloadId": { + "type": "string", + "nullable": true + }, + "protocol": { + "$ref": "#/components/schemas/DownloadProtocol" + }, + "downloadClient": { + "type": "string", + "nullable": true + }, + "indexer": { + "type": "string", + "nullable": true + }, + "outputPath": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "QueueResourcePagingResource": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "format": "int32" + }, + "sortKey": { + "type": "string", + "nullable": true + }, + "sortDirection": { + "$ref": "#/components/schemas/SortDirection" + }, + "filters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PagingResourceFilter" + }, + "nullable": true + }, + "totalRecords": { + "type": "integer", + "format": "int32" + }, + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueResource" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "QueueStatusResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "totalCount": { + "type": "integer", + "format": "int32" + }, + "count": { + "type": "integer", + "format": "int32" + }, + "unknownCount": { + "type": "integer", + "format": "int32" + }, + "errors": { + "type": "boolean" + }, + "warnings": { + "type": "boolean" + }, + "unknownErrors": { + "type": "boolean" + }, + "unknownWarnings": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "RatingChild": { + "type": "object", + "properties": { + "votes": { + "type": "integer", + "format": "int32" + }, + "value": { + "type": "number", + "format": "double" + }, + "type": { + "$ref": "#/components/schemas/RatingType" + } + }, + "additionalProperties": false + }, + "RatingType": { + "enum": [ + "user", + "critic" + ], + "type": "string" + }, + "Ratings": { + "type": "object", + "properties": { + "imdb": { + "$ref": "#/components/schemas/RatingChild" + }, + "tmdb": { + "$ref": "#/components/schemas/RatingChild" + }, + "metacritic": { + "$ref": "#/components/schemas/RatingChild" + }, + "rottenTomatoes": { + "$ref": "#/components/schemas/RatingChild" + } + }, + "additionalProperties": false + }, + "Rejection": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/RejectionType" + } + }, + "additionalProperties": false + }, + "RejectionType": { + "enum": [ + "permanent", + "temporary" + ], + "type": "string" + }, + "ReleaseResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "guid": { + "type": "string", + "nullable": true + }, + "quality": { + "$ref": "#/components/schemas/QualityModel" + }, + "customFormats": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatResource" + }, + "nullable": true + }, + "customFormatScore": { + "type": "integer", + "format": "int32" + }, + "qualityWeight": { + "type": "integer", + "format": "int32" + }, + "age": { + "type": "integer", + "format": "int32" + }, + "ageHours": { + "type": "number", + "format": "double" + }, + "ageMinutes": { + "type": "number", + "format": "double" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "indexerId": { + "type": "integer", + "format": "int32" + }, + "indexer": { + "type": "string", + "nullable": true + }, + "releaseGroup": { + "type": "string", + "nullable": true + }, + "subGroup": { + "type": "string", + "nullable": true + }, + "releaseHash": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "sceneSource": { + "type": "boolean" + }, + "movieTitles": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Language" + }, + "nullable": true + }, + "approved": { + "type": "boolean" + }, + "temporarilyRejected": { + "type": "boolean" + }, + "rejected": { + "type": "boolean" + }, + "tmdbId": { + "type": "integer", + "format": "int32" + }, + "imdbId": { + "type": "integer", + "format": "int32" + }, + "rejections": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "publishDate": { + "type": "string", + "format": "date-time" + }, + "commentUrl": { + "type": "string", + "nullable": true + }, + "downloadUrl": { + "type": "string", + "nullable": true + }, + "infoUrl": { + "type": "string", + "nullable": true + }, + "downloadAllowed": { + "type": "boolean" + }, + "releaseWeight": { + "type": "integer", + "format": "int32" + }, + "indexerFlags": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "edition": { + "type": "string", + "nullable": true + }, + "magnetUrl": { + "type": "string", + "nullable": true + }, + "infoHash": { + "type": "string", + "nullable": true + }, + "seeders": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "leechers": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "protocol": { + "$ref": "#/components/schemas/DownloadProtocol" + }, + "movieId": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "RemotePathMappingResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "host": { + "type": "string", + "nullable": true + }, + "remotePath": { + "type": "string", + "nullable": true + }, + "localPath": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "RenameMovieResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "movieId": { + "type": "integer", + "format": "int32" + }, + "movieFileId": { + "type": "integer", + "format": "int32" + }, + "existingPath": { + "type": "string", + "nullable": true + }, + "newPath": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "RescanAfterRefreshType": { + "enum": [ + "always", + "afterManual", + "never" + ], + "type": "string" + }, + "RestrictionResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "required": { + "type": "string", + "nullable": true + }, + "preferred": { + "type": "string", + "nullable": true + }, + "ignored": { + "type": "string", + "nullable": true + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "Revision": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "format": "int32" + }, + "real": { + "type": "integer", + "format": "int32" + }, + "isRepack": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "RootFolderResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "path": { + "type": "string", + "nullable": true + }, + "accessible": { + "type": "boolean" + }, + "freeSpace": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "unmappedFolders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnmappedFolder" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "SelectOption": { + "type": "object", + "properties": { + "value": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "order": { + "type": "integer", + "format": "int32" + }, + "hint": { + "type": "string", + "nullable": true + }, + "dividerAfter": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "SortDirection": { + "enum": [ + "default", + "ascending", + "descending" + ], + "type": "string" + }, + "Source": { + "enum": [ + "unknown", + "cam", + "telesync", + "telecine", + "workprint", + "dvd", + "tv", + "webdl", + "webrip", + "bluray" + ], + "type": "string" + }, + "SourceType": { + "enum": [ + "tmdb", + "mappings", + "user", + "indexer" + ], + "type": "string" + }, + "TMDbCountryCode": { + "enum": [ + "au", + "br", + "ca", + "fr", + "de", + "gb", + "it", + "es", + "us", + "nz" + ], + "type": "string" + }, + "TagDetailsResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "label": { + "type": "string", + "nullable": true + }, + "delayProfileIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "notificationIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "restrictionIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "importListIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "movieIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "indexerIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "TagResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "label": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "TaskResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "taskName": { + "type": "string", + "nullable": true + }, + "interval": { + "type": "integer", + "format": "int32" + }, + "lastExecution": { + "type": "string", + "format": "date-time" + }, + "lastStartTime": { + "type": "string", + "format": "date-time" + }, + "nextExecution": { + "type": "string", + "format": "date-time" + }, + "lastDuration": { + "$ref": "#/components/schemas/TimeSpan" + } + }, + "additionalProperties": false + }, + "TimeSpan": { + "type": "object", + "properties": { + "ticks": { + "type": "integer", + "format": "int64" + }, + "days": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "hours": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "milliseconds": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "minutes": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "seconds": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "totalDays": { + "type": "number", + "format": "double", + "readOnly": true + }, + "totalHours": { + "type": "number", + "format": "double", + "readOnly": true + }, + "totalMilliseconds": { + "type": "number", + "format": "double", + "readOnly": true + }, + "totalMinutes": { + "type": "number", + "format": "double", + "readOnly": true + }, + "totalSeconds": { + "type": "number", + "format": "double", + "readOnly": true + } + }, + "additionalProperties": false + }, + "TrackedDownloadState": { + "enum": [ + "downloading", + "importPending", + "importing", + "imported", + "failedPending", + "failed", + "ignored" + ], + "type": "string" + }, + "TrackedDownloadStatus": { + "enum": [ + "ok", + "warning", + "error" + ], + "type": "string" + }, + "TrackedDownloadStatusMessage": { + "type": "object", + "properties": { + "title": { + "type": "string", + "nullable": true + }, + "messages": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "UiConfigResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "firstDayOfWeek": { + "type": "integer", + "format": "int32" + }, + "calendarWeekColumnHeader": { + "type": "string", + "nullable": true + }, + "movieRuntimeFormat": { + "$ref": "#/components/schemas/MovieRuntimeFormatType" + }, + "shortDateFormat": { + "type": "string", + "nullable": true + }, + "longDateFormat": { + "type": "string", + "nullable": true + }, + "timeFormat": { + "type": "string", + "nullable": true + }, + "showRelativeDates": { + "type": "boolean" + }, + "enableColorImpairedMode": { + "type": "boolean" + }, + "movieInfoLanguage": { + "type": "integer", + "format": "int32" + }, + "uiLanguage": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "UnmappedFolder": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "path": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "UpdateChanges": { + "type": "object", + "properties": { + "new": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "fixed": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "UpdateMechanism": { + "enum": [ + "builtIn", + "script", + "external", + "apt", + "docker" + ], + "type": "string" + }, + "UpdateResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "version": { + "$ref": "#/components/schemas/Version" + }, + "branch": { + "type": "string", + "nullable": true + }, + "releaseDate": { + "type": "string", + "format": "date-time" + }, + "fileName": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string", + "nullable": true + }, + "installed": { + "type": "boolean" + }, + "installedOn": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "installable": { + "type": "boolean" + }, + "latest": { + "type": "boolean" + }, + "changes": { + "$ref": "#/components/schemas/UpdateChanges" + }, + "hash": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "Version": { + "type": "object", + "properties": { + "major": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "minor": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "build": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "revision": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "majorRevision": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "minorRevision": { + "type": "integer", + "format": "int32", + "readOnly": true + } + }, + "additionalProperties": false + } + }, + "securitySchemes": { + "X-Api-Key": { + "type": "apiKey", + "description": "Apikey passed as header", + "name": "X-Api-Key", + "in": "header" + }, + "apikey": { + "type": "apiKey", + "description": "Apikey passed as header", + "name": "apikey", + "in": "query" + } + } + }, + "security": [ + { + "X-Api-Key": [ ] + }, + { + "apikey": [ ] + } + ] +} \ No newline at end of file diff --git a/src/Radarr.Http/Frontend/InitializeJsController.cs b/src/Radarr.Http/Frontend/InitializeJsController.cs index 1931f8b4c5..07cd89a59c 100644 --- a/src/Radarr.Http/Frontend/InitializeJsController.cs +++ b/src/Radarr.Http/Frontend/InitializeJsController.cs @@ -44,7 +44,7 @@ private string GetContent() var builder = new StringBuilder(); builder.AppendLine("window.Radarr = {"); - builder.AppendLine($" apiRoot: '{_urlBase}/api/v3',"); + builder.AppendLine($" apiRoot: '{_urlBase}/api/v4',"); builder.AppendLine($" apiKey: '{_apiKey}',"); builder.AppendLine($" release: '{BuildInfo.Release}',"); builder.AppendLine($" version: '{BuildInfo.Version.ToString()}',"); diff --git a/src/Radarr.Http/VersionedApiControllerAttribute.cs b/src/Radarr.Http/VersionedApiControllerAttribute.cs index 74539bf4bf..a99b0b0048 100644 --- a/src/Radarr.Http/VersionedApiControllerAttribute.cs +++ b/src/Radarr.Http/VersionedApiControllerAttribute.cs @@ -31,4 +31,12 @@ public V3ApiControllerAttribute(string resource = "[controller]") { } } + + public class V4ApiControllerAttribute : VersionedApiControllerAttribute + { + public V4ApiControllerAttribute(string resource = "[controller]") + : base(4, resource) + { + } + } } diff --git a/src/Radarr.Http/VersionedFeedControllerAttribute.cs b/src/Radarr.Http/VersionedFeedControllerAttribute.cs index d1ff9e9bd2..d572b11a2d 100644 --- a/src/Radarr.Http/VersionedFeedControllerAttribute.cs +++ b/src/Radarr.Http/VersionedFeedControllerAttribute.cs @@ -24,4 +24,12 @@ public V3FeedControllerAttribute(string resource = "[controller]") { } } + + public class V4FeedControllerAttribute : VersionedFeedControllerAttribute + { + public V4FeedControllerAttribute(string resource = "[controller]") + : base(4, resource) + { + } + } } diff --git a/src/Radarr.sln b/src/Radarr.sln index 6d0f899123..7206becd3e 100644 --- a/src/Radarr.sln +++ b/src/Radarr.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29418.71 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32825.248 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{57A04B72-8088-4F75-A582-1158CF8291F7}" EndProject @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{4E EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Radarr.Api.V3", "Radarr.Api.V3\Radarr.Api.V3.csproj", "{D1D48E1D-9EEB-470B-992C-3954F90FB014}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Radarr.Api.V4", "Radarr.Api.V4\Radarr.Api.V4.csproj", "{97071405-1CD1-4D81-B0C3-A37BB86ACF05}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Radarr.Http", "Radarr.Http\Radarr.Http.csproj", "{F8A02FD4-A7A4-40D0-BB81-6319105A3302}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Radarr.Api.Test", "NzbDrone.Api.Test\Radarr.Api.Test.csproj", "{E2EA47B1-6996-417D-A6EC-28C4F202715C}" @@ -276,6 +278,14 @@ Global {0E2B067C-4A97-430C-8767-D19ACB09A63A}.Release|Posix.Build.0 = Release|Any CPU {0E2B067C-4A97-430C-8767-D19ACB09A63A}.Release|Windows.ActiveCfg = Release|Any CPU {0E2B067C-4A97-430C-8767-D19ACB09A63A}.Release|Windows.Build.0 = Release|Any CPU + {97071405-1CD1-4D81-B0C3-A37BB86ACF05}.Debug|Posix.ActiveCfg = Debug|Any CPU + {97071405-1CD1-4D81-B0C3-A37BB86ACF05}.Debug|Posix.Build.0 = Debug|Any CPU + {97071405-1CD1-4D81-B0C3-A37BB86ACF05}.Debug|Windows.ActiveCfg = Debug|Any CPU + {97071405-1CD1-4D81-B0C3-A37BB86ACF05}.Debug|Windows.Build.0 = Debug|Any CPU + {97071405-1CD1-4D81-B0C3-A37BB86ACF05}.Release|Posix.ActiveCfg = Release|Any CPU + {97071405-1CD1-4D81-B0C3-A37BB86ACF05}.Release|Posix.Build.0 = Release|Any CPU + {97071405-1CD1-4D81-B0C3-A37BB86ACF05}.Release|Windows.ActiveCfg = Release|Any CPU + {97071405-1CD1-4D81-B0C3-A37BB86ACF05}.Release|Windows.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE