From c247d07e8411c2cc48b16cf5afef8b5d769fd283 Mon Sep 17 00:00:00 2001 From: ta264 Date: Wed, 4 Aug 2021 21:42:40 +0100 Subject: [PATCH] New: Use ASP.NET Core instead of Nancy --- .../Actions/Settings/qualityDefinitions.js | 4 +- .../src/Store/Actions/albumHistoryActions.js | 2 +- .../src/Store/Actions/artistHistoryActions.js | 2 +- .../src/Store/Actions/blacklistActions.js | 1 + frontend/src/Store/Actions/commandActions.js | 3 +- frontend/src/Store/Actions/historyActions.js | 1 + frontend/src/Store/Actions/queueActions.js | 2 +- frontend/src/Store/Actions/releaseActions.js | 1 + frontend/src/Store/Actions/searchActions.js | 1 + frontend/src/Store/Actions/tagActions.js | 3 +- frontend/src/Utilities/createAjaxRequest.js | 15 +- ...udioModule.cs => AlbumStudioController.cs} | 18 +- .../{AlbumModule.cs => AlbumController.cs} | 74 ++-- ...gnalR.cs => AlbumControllerWithSignalR.cs} | 26 +- ...okupModule.cs => AlbumLookupController.cs} | 14 +- .../{ArtistModule.cs => ArtistController.cs} | 46 ++- ...torModule.cs => ArtistEditorController.cs} | 25 +- .../Artist/ArtistImportModule.cs | 28 -- ...kupModule.cs => ArtistLookupController.cs} | 14 +- .../Blacklist/BlacklistController.cs | 43 ++ .../Blacklist/BlacklistModule.cs | 42 -- .../Calendar/CalendarController.cs | 42 ++ ...eedModule.cs => CalendarFeedController.cs} | 42 +- src/Lidarr.Api.V1/Calendar/CalendarModule.cs | 54 --- ...{CommandModule.cs => CommandController.cs} | 48 ++- src/Lidarr.Api.V1/Config/ConfigController.cs | 48 +++ ...e.cs => DownloadClientConfigController.cs} | 6 +- ...onfigModule.cs => HostConfigController.cs} | 37 +- ...igModule.cs => IndexerConfigController.cs} | 6 +- .../Config/LidarrConfigModule.cs | 53 --- ....cs => MediaManagementConfigController.cs} | 6 +- ...cs => MetadataProviderConfigController.cs} | 6 +- ...figModule.cs => NamingConfigController.cs} | 44 ++- ...iConfigModule.cs => UiConfigController.cs} | 8 +- .../CustomFilters/CustomFilterController.cs | 52 +++ .../CustomFilters/CustomFilterModule.cs | 49 --- ...kSpaceModule.cs => DiskSpaceController.cs} | 11 +- ...tModule.cs => DownloadClientController.cs} | 8 +- ...ystemModule.cs => FileSystemController.cs} | 34 +- .../{HealthModule.cs => HealthController.cs} | 20 +- ...{HistoryModule.cs => HistoryController.cs} | 77 +--- ...tListModule.cs => ImportListController.cs} | 6 +- ...le.cs => ImportListExclusionController.cs} | 31 +- ...{IndexerModule.cs => IndexerController.cs} | 8 +- ...{ReleaseModule.cs => ReleaseController.cs} | 29 +- ...ModuleBase.cs => ReleaseControllerBase.cs} | 10 +- ...PushModule.cs => ReleasePushController.cs} | 14 +- src/Lidarr.Api.V1/Lidarr.Api.V1.csproj | 4 +- src/Lidarr.Api.V1/LidarrV1FeedModule.cs | 12 - src/Lidarr.Api.V1/LidarrV1Module.cs | 12 - .../Logs/{LogModule.cs => LogController.cs} | 14 +- ...{LogFileModule.cs => LogFileController.cs} | 8 +- ...ModuleBase.cs => LogFileControllerBase.cs} | 30 +- ...leModule.cs => UpdateLogFileController.cs} | 10 +- ...ortModule.cs => ManualImportController.cs} | 35 +- ...CoverModule.cs => MediaCoverController.cs} | 43 +- ...etadataModule.cs => MetadataController.cs} | 8 +- ...ionModule.cs => NotificationController.cs} | 8 +- .../{ParseModule.cs => ParseController.cs} | 12 +- ...ileModule.cs => DelayProfileController.cs} | 39 +- ...Module.cs => MetadataProfileController.cs} | 33 +- ....cs => MetadataProfileSchemaController.cs} | 13 +- ...eModule.cs => QualityProfileController.cs} | 32 +- ...e.cs => QualityProfileSchemaController.cs} | 11 +- ...eModule.cs => ReleaseProfileController.cs} | 31 +- ...oduleBase.cs => ProviderControllerBase.cs} | 66 ++-- .../Qualities/QualityDefinitionController.cs | 54 +++ .../Qualities/QualityDefinitionModule.cs | 54 --- .../Queue/QueueActionController.cs | 55 +++ src/Lidarr.Api.V1/Queue/QueueActionModule.cs | 184 --------- .../{QueueModule.cs => QueueController.cs} | 138 ++++++- ...ilsModule.cs => QueueDetailsController.cs} | 38 +- ...atusModule.cs => QueueStatusController.cs} | 20 +- ...dule.cs => RemotePathMappingController.cs} | 34 +- ...olderModule.cs => RootFolderController.cs} | 31 +- .../{SearchModule.cs => SearchController.cs} | 14 +- .../{BackupModule.cs => BackupController.cs} | 25 +- .../{SystemModule.cs => SystemController.cs} | 68 ++-- .../{TaskModule.cs => TaskController.cs} | 17 +- .../Tags/{TagModule.cs => TagController.cs} | 32 +- ...tailsModule.cs => TagDetailsController.cs} | 16 +- ...ckFileModule.cs => TrackFileController.cs} | 79 ++-- .../Tracks/RenameTrackController.cs | 29 ++ src/Lidarr.Api.V1/Tracks/RenameTrackModule.cs | 42 -- .../Tracks/RetagTrackController.cs | 30 ++ src/Lidarr.Api.V1/Tracks/RetagTrackModule.cs | 39 -- src/Lidarr.Api.V1/Tracks/TrackController.cs | 52 +++ ...gnalR.cs => TrackControllerWithSignalR.cs} | 27 +- src/Lidarr.Api.V1/Tracks/TrackModule.cs | 65 --- .../{UpdateModule.cs => UpdateController.cs} | 12 +- .../{CutoffModule.cs => CutoffController.cs} | 18 +- ...{MissingModule.cs => MissingController.cs} | 18 +- .../ApiKeyAuthenticationHandler.cs | 89 +++++ .../AuthenticationBuilderExtensions.cs | 65 +++ .../AuthenticationController.cs | 58 +++ .../Authentication/AuthenticationModule.cs | 50 --- .../Authentication/AuthenticationService.cs | 184 +-------- .../BasicAuthenticationHandler.cs | 84 ++++ .../Authentication/EnableAuthInNancy.cs | 142 ------- .../Authentication/LidarrNancyCookie.cs | 38 -- .../Authentication/LoginResource.cs | 4 +- .../Authentication/NoAuthenticationHandler.cs | 37 ++ .../UiAuthorizationPolicyProvider.cs | 39 ++ .../ErrorManagement/ErrorHandler.cs | 41 -- src/Lidarr.Http/ErrorManagement/ErrorModel.cs | 13 +- .../ErrorManagement/LidarrErrorPipeline.cs | 80 ++-- src/Lidarr.Http/Exceptions/ApiException.cs | 10 +- .../Extensions/NancyJsonSerializer.cs | 31 -- .../Pipelines/CacheHeaderPipeline.cs | 40 -- .../Extensions/Pipelines/CorsPipeline.cs | 80 ---- .../Extensions/Pipelines/GZipPipeline.cs | 103 ----- .../Pipelines/IRegisterNancyPipeline.cs | 11 - .../Pipelines/IfModifiedPipeline.cs | 35 -- .../Pipelines/LidarrVersionPipeline.cs | 24 -- .../Pipelines/RequestLoggingPipeline.cs | 105 ----- .../Pipelines/SetCookieHeaderPipeline.cs | 30 -- .../Extensions/Pipelines/UrlBasePipeline.cs | 45 --- .../Extensions/ReqResExtensions.cs | 62 --- .../Extensions/RequestExtensions.cs | 192 ++++++--- .../Frontend/CacheableSpecification.cs | 74 ---- ...eJsModule.cs => InitializeJsController.cs} | 34 +- .../Frontend/Mappers/HtmlMapperBase.cs | 6 +- .../Mappers/IMapHttpRequestsToDisk.cs | 4 +- .../Mappers/StaticResourceMapperBase.cs | 20 +- .../Frontend/StaticResourceController.cs | 73 ++++ .../Frontend/StaticResourceModule.cs | 48 --- src/Lidarr.Http/Lidarr.Http.csproj | 4 +- src/Lidarr.Http/LidarrBootstrapper.cs | 75 ---- src/Lidarr.Http/LidarrModule.cs | 18 - src/Lidarr.Http/LidarrRestModule.cs | 57 --- .../Middleware/CacheHeaderMiddleware.cs | 35 ++ .../Middleware/CacheableSpecification.cs | 74 ++++ .../Middleware/IfModifiedMiddleware.cs | 43 ++ .../Middleware/LoggingMiddleware.cs | 92 +++++ .../Middleware/UrlBaseMiddleware.cs | 29 ++ .../Middleware/VersionMiddleware.cs | 30 ++ .../Attributes/RestDeleteByIdAttribute.cs | 14 + .../REST/Attributes/RestGetByIdAttribute.cs | 21 + .../REST/Attributes/RestPostByIdAttribute.cs | 10 + .../REST/Attributes/RestPutByIdAttribute.cs | 14 + .../Attributes/SkipValidationAttribute.cs | 17 + src/Lidarr.Http/REST/BadRequestException.cs | 4 +- .../REST/MethodNotAllowedException.cs | 4 +- src/Lidarr.Http/REST/NotFoundException.cs | 4 +- src/Lidarr.Http/REST/RestController.cs | 130 ++++++ .../RestControllerWithSignalR.cs} | 25 +- src/Lidarr.Http/REST/RestModule.cs | 373 ------------------ .../REST/UnsupportedMediaTypeException.cs | 2 +- src/Lidarr.Http/TinyIoCNancyBootstrapper.cs | 273 ------------- .../Validation/DuplicateEndpointDetector.cs | 121 ++++++ .../VersionedApiControllerAttribute.cs | 34 ++ .../VersionedFeedControllerAttribute.cs | 27 ++ .../Serializer/System.Text.Json/STJson.cs | 35 +- src/NzbDrone.Host/MainAppContainerBuilder.cs | 4 - .../WebHost/ControllerActivator.cs | 26 ++ .../Middleware/IAspNetCoreMiddleware.cs | 10 - .../WebHost/Middleware/NancyMiddleware.cs | 29 -- .../WebHost/Middleware/SignalRMiddleware.cs | 70 ---- .../WebHost/WebHostController.cs | 138 ++++++- .../Client/ClientBase.cs | 4 +- src/NzbDrone.Integration.Test/CorsFixture.cs | 3 + 161 files changed, 2877 insertions(+), 3649 deletions(-) rename src/Lidarr.Api.V1/AlbumStudio/{AlbumStudioModule.cs => AlbumStudioController.cs} (68%) rename src/Lidarr.Api.V1/Albums/{AlbumModule.cs => AlbumController.cs} (76%) rename src/Lidarr.Api.V1/Albums/{AlbumModuleWithSignalR.cs => AlbumControllerWithSignalR.cs} (78%) rename src/Lidarr.Api.V1/Albums/{AlbumLookupModule.cs => AlbumLookupController.cs} (72%) rename src/Lidarr.Api.V1/Artist/{ArtistModule.cs => ArtistController.cs} (91%) rename src/Lidarr.Api.V1/Artist/{ArtistEditorModule.cs => ArtistEditorController.cs} (78%) delete mode 100644 src/Lidarr.Api.V1/Artist/ArtistImportModule.cs rename src/Lidarr.Api.V1/Artist/{ArtistLookupModule.cs => ArtistLookupController.cs} (78%) create mode 100644 src/Lidarr.Api.V1/Blacklist/BlacklistController.cs delete mode 100644 src/Lidarr.Api.V1/Blacklist/BlacklistModule.cs create mode 100644 src/Lidarr.Api.V1/Calendar/CalendarController.cs rename src/Lidarr.Api.V1/Calendar/{CalendarFeedModule.cs => CalendarFeedController.cs} (64%) delete mode 100644 src/Lidarr.Api.V1/Calendar/CalendarModule.cs rename src/Lidarr.Api.V1/Commands/{CommandModule.cs => CommandController.cs} (66%) create mode 100644 src/Lidarr.Api.V1/Config/ConfigController.cs rename src/Lidarr.Api.V1/Config/{DownloadClientConfigModule.cs => DownloadClientConfigController.cs} (57%) rename src/Lidarr.Api.V1/Config/{HostConfigModule.cs => HostConfigController.cs} (83%) rename src/Lidarr.Api.V1/Config/{IndexerConfigModule.cs => IndexerConfigController.cs} (79%) delete mode 100644 src/Lidarr.Api.V1/Config/LidarrConfigModule.cs rename src/Lidarr.Api.V1/Config/{MediaManagementConfigModule.cs => MediaManagementConfigController.cs} (74%) rename src/Lidarr.Api.V1/Config/{MetadataProviderConfigModule.cs => MetadataProviderConfigController.cs} (68%) rename src/Lidarr.Api.V1/Config/{NamingConfigModule.cs => NamingConfigController.cs} (79%) rename src/Lidarr.Api.V1/Config/{UiConfigModule.cs => UiConfigController.cs} (53%) create mode 100644 src/Lidarr.Api.V1/CustomFilters/CustomFilterController.cs delete mode 100644 src/Lidarr.Api.V1/CustomFilters/CustomFilterModule.cs rename src/Lidarr.Api.V1/DiskSpace/{DiskSpaceModule.cs => DiskSpaceController.cs} (62%) rename src/Lidarr.Api.V1/DownloadClient/{DownloadClientModule.cs => DownloadClientController.cs} (64%) rename src/Lidarr.Api.V1/FileSystem/{FileSystemModule.cs => FileSystemController.cs} (54%) rename src/Lidarr.Api.V1/Health/{HealthModule.cs => HealthController.cs} (54%) rename src/Lidarr.Api.V1/History/{HistoryModule.cs => HistoryController.cs} (53%) rename src/Lidarr.Api.V1/ImportLists/{ImportListModule.cs => ImportListController.cs} (84%) rename src/Lidarr.Api.V1/ImportLists/{ImportListExclusionModule.cs => ImportListExclusionController.cs} (56%) rename src/Lidarr.Api.V1/Indexers/{IndexerModule.cs => IndexerController.cs} (67%) rename src/Lidarr.Api.V1/Indexers/{ReleaseModule.cs => ReleaseController.cs} (91%) rename src/Lidarr.Api.V1/Indexers/{ReleaseModuleBase.cs => ReleaseControllerBase.cs} (82%) rename src/Lidarr.Api.V1/Indexers/{ReleasePushModule.cs => ReleasePushController.cs} (90%) delete mode 100644 src/Lidarr.Api.V1/LidarrV1FeedModule.cs delete mode 100644 src/Lidarr.Api.V1/LidarrV1Module.cs rename src/Lidarr.Api.V1/Logs/{LogModule.cs => LogController.cs} (82%) rename src/Lidarr.Api.V1/Logs/{LogFileModule.cs => LogFileController.cs} (83%) rename src/Lidarr.Api.V1/Logs/{LogFileModuleBase.cs => LogFileControllerBase.cs} (70%) rename src/Lidarr.Api.V1/Logs/{UpdateLogFileModule.cs => UpdateLogFileController.cs} (83%) rename src/Lidarr.Api.V1/ManualImport/{ManualImportModule.cs => ManualImportController.cs} (71%) rename src/Lidarr.Api.V1/MediaCovers/{MediaCoverModule.cs => MediaCoverController.cs} (62%) rename src/Lidarr.Api.V1/Metadata/{MetadataModule.cs => MetadataController.cs} (65%) rename src/Lidarr.Api.V1/Notifications/{NotificationModule.cs => NotificationController.cs} (64%) rename src/Lidarr.Api.V1/Parse/{ParseModule.cs => ParseController.cs} (80%) rename src/Lidarr.Api.V1/Profiles/Delay/{DelayProfileModule.cs => DelayProfileController.cs} (65%) rename src/Lidarr.Api.V1/Profiles/Metadata/{MetadataProfileModule.cs => MetadataProfileController.cs} (59%) rename src/Lidarr.Api.V1/Profiles/Metadata/{MetadataProfileSchemaModule.cs => MetadataProfileSchemaController.cs} (82%) rename src/Lidarr.Api.V1/Profiles/Quality/{QualityProfileModule.cs => QualityProfileController.cs} (54%) rename src/Lidarr.Api.V1/Profiles/Quality/{QualityProfileSchemaModule.cs => QualityProfileSchemaController.cs} (57%) rename src/Lidarr.Api.V1/Profiles/Release/{ReleaseProfileModule.cs => ReleaseProfileController.cs} (66%) rename src/Lidarr.Api.V1/{ProviderModuleBase.cs => ProviderControllerBase.cs} (75%) create mode 100644 src/Lidarr.Api.V1/Qualities/QualityDefinitionController.cs delete mode 100644 src/Lidarr.Api.V1/Qualities/QualityDefinitionModule.cs create mode 100644 src/Lidarr.Api.V1/Queue/QueueActionController.cs delete mode 100644 src/Lidarr.Api.V1/Queue/QueueActionModule.cs rename src/Lidarr.Api.V1/Queue/{QueueModule.cs => QueueController.cs} (56%) rename src/Lidarr.Api.V1/Queue/{QueueDetailsModule.cs => QueueDetailsController.cs} (52%) rename src/Lidarr.Api.V1/Queue/{QueueStatusModule.cs => QueueStatusController.cs} (77%) rename src/Lidarr.Api.V1/RemotePathMappings/{RemotePathMappingModule.cs => RemotePathMappingController.cs} (60%) rename src/Lidarr.Api.V1/RootFolders/{RootFolderModule.cs => RootFolderController.cs} (76%) rename src/Lidarr.Api.V1/Search/{SearchModule.cs => SearchController.cs} (88%) rename src/Lidarr.Api.V1/System/Backup/{BackupModule.cs => BackupController.cs} (84%) rename src/Lidarr.Api.V1/System/{SystemModule.cs => SystemController.cs} (61%) rename src/Lidarr.Api.V1/System/Tasks/{TaskModule.cs => TaskController.cs} (76%) rename src/Lidarr.Api.V1/Tags/{TagModule.cs => TagController.cs} (51%) rename src/Lidarr.Api.V1/Tags/{TagDetailsModule.cs => TagDetailsController.cs} (53%) rename src/Lidarr.Api.V1/TrackFiles/{TrackFileModule.cs => TrackFileController.cs} (66%) create mode 100644 src/Lidarr.Api.V1/Tracks/RenameTrackController.cs delete mode 100644 src/Lidarr.Api.V1/Tracks/RenameTrackModule.cs create mode 100644 src/Lidarr.Api.V1/Tracks/RetagTrackController.cs delete mode 100644 src/Lidarr.Api.V1/Tracks/RetagTrackModule.cs create mode 100644 src/Lidarr.Api.V1/Tracks/TrackController.cs rename src/Lidarr.Api.V1/Tracks/{TrackModuleWithSignalR.cs => TrackControllerWithSignalR.cs} (79%) delete mode 100644 src/Lidarr.Api.V1/Tracks/TrackModule.cs rename src/Lidarr.Api.V1/Update/{UpdateModule.cs => UpdateController.cs} (79%) rename src/Lidarr.Api.V1/Wanted/{CutoffModule.cs => CutoffController.cs} (70%) rename src/Lidarr.Api.V1/Wanted/{MissingModule.cs => MissingController.cs} (69%) create mode 100644 src/Lidarr.Http/Authentication/ApiKeyAuthenticationHandler.cs create mode 100644 src/Lidarr.Http/Authentication/AuthenticationBuilderExtensions.cs create mode 100644 src/Lidarr.Http/Authentication/AuthenticationController.cs delete mode 100644 src/Lidarr.Http/Authentication/AuthenticationModule.cs create mode 100644 src/Lidarr.Http/Authentication/BasicAuthenticationHandler.cs delete mode 100644 src/Lidarr.Http/Authentication/EnableAuthInNancy.cs delete mode 100644 src/Lidarr.Http/Authentication/LidarrNancyCookie.cs create mode 100644 src/Lidarr.Http/Authentication/NoAuthenticationHandler.cs create mode 100644 src/Lidarr.Http/Authentication/UiAuthorizationPolicyProvider.cs delete mode 100644 src/Lidarr.Http/ErrorManagement/ErrorHandler.cs delete mode 100644 src/Lidarr.Http/Extensions/NancyJsonSerializer.cs delete mode 100644 src/Lidarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs delete mode 100644 src/Lidarr.Http/Extensions/Pipelines/CorsPipeline.cs delete mode 100644 src/Lidarr.Http/Extensions/Pipelines/GZipPipeline.cs delete mode 100644 src/Lidarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs delete mode 100644 src/Lidarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs delete mode 100644 src/Lidarr.Http/Extensions/Pipelines/LidarrVersionPipeline.cs delete mode 100644 src/Lidarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs delete mode 100644 src/Lidarr.Http/Extensions/Pipelines/SetCookieHeaderPipeline.cs delete mode 100644 src/Lidarr.Http/Extensions/Pipelines/UrlBasePipeline.cs delete mode 100644 src/Lidarr.Http/Extensions/ReqResExtensions.cs delete mode 100644 src/Lidarr.Http/Frontend/CacheableSpecification.cs rename src/Lidarr.Http/Frontend/{InitializeJsModule.cs => InitializeJsController.cs} (72%) create mode 100644 src/Lidarr.Http/Frontend/StaticResourceController.cs delete mode 100644 src/Lidarr.Http/Frontend/StaticResourceModule.cs delete mode 100644 src/Lidarr.Http/LidarrBootstrapper.cs delete mode 100644 src/Lidarr.Http/LidarrModule.cs delete mode 100644 src/Lidarr.Http/LidarrRestModule.cs create mode 100644 src/Lidarr.Http/Middleware/CacheHeaderMiddleware.cs create mode 100644 src/Lidarr.Http/Middleware/CacheableSpecification.cs create mode 100644 src/Lidarr.Http/Middleware/IfModifiedMiddleware.cs create mode 100644 src/Lidarr.Http/Middleware/LoggingMiddleware.cs create mode 100644 src/Lidarr.Http/Middleware/UrlBaseMiddleware.cs create mode 100644 src/Lidarr.Http/Middleware/VersionMiddleware.cs create mode 100644 src/Lidarr.Http/REST/Attributes/RestDeleteByIdAttribute.cs create mode 100644 src/Lidarr.Http/REST/Attributes/RestGetByIdAttribute.cs create mode 100644 src/Lidarr.Http/REST/Attributes/RestPostByIdAttribute.cs create mode 100644 src/Lidarr.Http/REST/Attributes/RestPutByIdAttribute.cs create mode 100644 src/Lidarr.Http/REST/Attributes/SkipValidationAttribute.cs create mode 100644 src/Lidarr.Http/REST/RestController.cs rename src/Lidarr.Http/{LidarrRestModuleWithSignalR.cs => REST/RestControllerWithSignalR.cs} (76%) delete mode 100644 src/Lidarr.Http/REST/RestModule.cs delete mode 100644 src/Lidarr.Http/TinyIoCNancyBootstrapper.cs create mode 100644 src/Lidarr.Http/Validation/DuplicateEndpointDetector.cs create mode 100644 src/Lidarr.Http/VersionedApiControllerAttribute.cs create mode 100644 src/Lidarr.Http/VersionedFeedControllerAttribute.cs create mode 100644 src/NzbDrone.Host/WebHost/ControllerActivator.cs delete mode 100644 src/NzbDrone.Host/WebHost/Middleware/IAspNetCoreMiddleware.cs delete mode 100644 src/NzbDrone.Host/WebHost/Middleware/NancyMiddleware.cs delete mode 100644 src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs diff --git a/frontend/src/Store/Actions/Settings/qualityDefinitions.js b/frontend/src/Store/Actions/Settings/qualityDefinitions.js index b17ec01de..09317ca07 100644 --- a/frontend/src/Store/Actions/Settings/qualityDefinitions.js +++ b/frontend/src/Store/Actions/Settings/qualityDefinitions.js @@ -78,7 +78,9 @@ export default { const promise = createAjaxRequest({ method: 'PUT', url: '/qualityDefinition/update', - data: JSON.stringify(upatedDefinitions) + data: JSON.stringify(upatedDefinitions), + contentType: 'application/json', + dataType: 'json' }).request; promise.done((data) => { diff --git a/frontend/src/Store/Actions/albumHistoryActions.js b/frontend/src/Store/Actions/albumHistoryActions.js index 687bbb81b..12cb1e4c0 100644 --- a/frontend/src/Store/Actions/albumHistoryActions.js +++ b/frontend/src/Store/Actions/albumHistoryActions.js @@ -88,6 +88,7 @@ export const actionHandlers = handleThunks({ const promise = createAjaxRequest({ url: '/history/failed', method: 'POST', + dataType: 'json', data: { id: historyId } @@ -109,4 +110,3 @@ export const reducers = createHandleActions({ } }, defaultState, section); - diff --git a/frontend/src/Store/Actions/artistHistoryActions.js b/frontend/src/Store/Actions/artistHistoryActions.js index b963d5984..b023cfaf1 100644 --- a/frontend/src/Store/Actions/artistHistoryActions.js +++ b/frontend/src/Store/Actions/artistHistoryActions.js @@ -80,6 +80,7 @@ export const actionHandlers = handleThunks({ const promise = createAjaxRequest({ url: '/history/failed', method: 'POST', + dataType: 'json', data: { id: historyId } @@ -101,4 +102,3 @@ export const reducers = createHandleActions({ } }, defaultState, section); - diff --git a/frontend/src/Store/Actions/blacklistActions.js b/frontend/src/Store/Actions/blacklistActions.js index 8fb8a9399..3a6f97a22 100644 --- a/frontend/src/Store/Actions/blacklistActions.js +++ b/frontend/src/Store/Actions/blacklistActions.js @@ -146,6 +146,7 @@ export const actionHandlers = handleThunks({ url: '/blacklist/bulk', method: 'DELETE', dataType: 'json', + contentType: 'application/json', data: JSON.stringify({ ids }) }).request; diff --git a/frontend/src/Store/Actions/commandActions.js b/frontend/src/Store/Actions/commandActions.js index 66d2ca6da..a31979d0d 100644 --- a/frontend/src/Store/Actions/commandActions.js +++ b/frontend/src/Store/Actions/commandActions.js @@ -139,7 +139,8 @@ export function executeCommandHelper( payload, dispatch) { const promise = createAjaxRequest({ url: '/command', method: 'POST', - data: JSON.stringify(payload) + data: JSON.stringify(payload), + dataType: 'json' }).request; return promise.then((data) => { diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 80ba58c3e..d8b524c6c 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -264,6 +264,7 @@ export const actionHandlers = handleThunks({ const promise = createAjaxRequest({ url: '/history/failed', method: 'POST', + dataType: 'json', data: { id } diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index c2f649408..13b3b0369 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -396,6 +396,7 @@ export const actionHandlers = handleThunks({ url: `/queue/bulk?removeFromClient=${remove}&blacklist=${blacklist}&skipredownload=${skipredownload}`, method: 'DELETE', dataType: 'json', + contentType: 'application/json', data: JSON.stringify({ ids }) }).request; @@ -453,4 +454,3 @@ export const reducers = createHandleActions({ }) }, defaultState, section); - diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index 61c936e5d..f59126ac9 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -271,6 +271,7 @@ export const actionHandlers = handleThunks({ const promise = createAjaxRequest({ url: '/release', method: 'POST', + dataType: 'json', contentType: 'application/json', data: JSON.stringify(payload) }).request; diff --git a/frontend/src/Store/Actions/searchActions.js b/frontend/src/Store/Actions/searchActions.js index ff990120b..f2c4e242b 100644 --- a/frontend/src/Store/Actions/searchActions.js +++ b/frontend/src/Store/Actions/searchActions.js @@ -115,6 +115,7 @@ export const actionHandlers = handleThunks({ const promise = createAjaxRequest({ url: '/artist', method: 'POST', + dataType: 'json', contentType: 'application/json', data: JSON.stringify(newArtist) }).request; diff --git a/frontend/src/Store/Actions/tagActions.js b/frontend/src/Store/Actions/tagActions.js index 3b92eb8a4..6800b1d58 100644 --- a/frontend/src/Store/Actions/tagActions.js +++ b/frontend/src/Store/Actions/tagActions.js @@ -53,7 +53,8 @@ export const actionHandlers = handleThunks({ const promise = createAjaxRequest({ url: '/tag', method: 'POST', - data: JSON.stringify(payload.tag) + data: JSON.stringify(payload.tag), + dataType: 'json' }).request; promise.done((data) => { diff --git a/frontend/src/Utilities/createAjaxRequest.js b/frontend/src/Utilities/createAjaxRequest.js index 3967c6748..d4836cadc 100644 --- a/frontend/src/Utilities/createAjaxRequest.js +++ b/frontend/src/Utilities/createAjaxRequest.js @@ -7,18 +7,6 @@ function isRelative(ajaxOptions) { return !absUrlRegex.test(ajaxOptions.url); } -function moveBodyToQuery(ajaxOptions) { - if (ajaxOptions.data && ajaxOptions.type === 'DELETE') { - if (ajaxOptions.url.contains('?')) { - ajaxOptions.url += '&'; - } else { - ajaxOptions.url += '?'; - } - ajaxOptions.url += $.param(ajaxOptions.data); - delete ajaxOptions.data; - } -} - function addRootUrl(ajaxOptions) { ajaxOptions.url = apiRoot + ajaxOptions.url; } @@ -32,7 +20,7 @@ function addContentType(ajaxOptions) { if ( ajaxOptions.contentType == null && ajaxOptions.dataType === 'json' && - (ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST')) { + (ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST' || ajaxOptions.method === 'DELETE')) { ajaxOptions.contentType = 'application/json'; } } @@ -52,7 +40,6 @@ export default function createAjaxRequest(originalAjaxOptions) { const ajaxOptions = { ...originalAjaxOptions }; if (isRelative(ajaxOptions)) { - moveBodyToQuery(ajaxOptions); addRootUrl(ajaxOptions); addApiKey(ajaxOptions); addContentType(ajaxOptions); diff --git a/src/Lidarr.Api.V1/AlbumStudio/AlbumStudioModule.cs b/src/Lidarr.Api.V1/AlbumStudio/AlbumStudioController.cs similarity index 68% rename from src/Lidarr.Api.V1/AlbumStudio/AlbumStudioModule.cs rename to src/Lidarr.Api.V1/AlbumStudio/AlbumStudioController.cs index 6f55d4d1a..2561d6d26 100644 --- a/src/Lidarr.Api.V1/AlbumStudio/AlbumStudioModule.cs +++ b/src/Lidarr.Api.V1/AlbumStudio/AlbumStudioController.cs @@ -1,27 +1,25 @@ using System.Linq; -using Lidarr.Http.Extensions; -using Nancy; +using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Music; namespace Lidarr.Api.V1.AlbumStudio { - public class AlbumStudioModule : LidarrV1Module + [V1ApiController] + public class AlbumStudioController : Controller { private readonly IArtistService _artistService; private readonly IAlbumMonitoredService _albumMonitoredService; - public AlbumStudioModule(IArtistService artistService, IAlbumMonitoredService albumMonitoredService) - : base("/albumstudio") + public AlbumStudioController(IArtistService artistService, IAlbumMonitoredService albumMonitoredService) { _artistService = artistService; _albumMonitoredService = albumMonitoredService; - Post("/", artist => UpdateAll()); } - private object UpdateAll() + [HttpPost] + public IActionResult UpdateAll([FromBody] AlbumStudioResource request) { - //Read from request - var request = Request.Body.FromJson(); var artistToUpdate = _artistService.GetArtists(request.Artist.Select(s => s.Id)); foreach (var s in request.Artist) @@ -41,7 +39,7 @@ namespace Lidarr.Api.V1.AlbumStudio _albumMonitoredService.SetAlbumMonitoredStatus(artist, request.MonitoringOptions); } - return ResponseWithCode("ok", HttpStatusCode.Accepted); + return Accepted(); } } } diff --git a/src/Lidarr.Api.V1/Albums/AlbumModule.cs b/src/Lidarr.Api.V1/Albums/AlbumController.cs similarity index 76% rename from src/Lidarr.Api.V1/Albums/AlbumModule.cs rename to src/Lidarr.Api.V1/Albums/AlbumController.cs index c6bd9b2f4..30dc2dc74 100644 --- a/src/Lidarr.Api.V1/Albums/AlbumModule.cs +++ b/src/Lidarr.Api.V1/Albums/AlbumController.cs @@ -1,9 +1,10 @@ -using System; using System.Collections.Generic; using System.Linq; using FluentValidation; +using Lidarr.Http; using Lidarr.Http.Extensions; -using Nancy; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.ArtistStats; using NzbDrone.Core.Datastore.Events; @@ -21,7 +22,8 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Albums { - public class AlbumModule : AlbumModuleWithSignalR, + [V1ApiController] + public class AlbumController : AlbumControllerWithSignalR, IHandle, IHandle, IHandle, @@ -33,7 +35,7 @@ namespace Lidarr.Api.V1.Albums protected readonly IReleaseService _releaseService; protected readonly IAddAlbumService _addAlbumService; - public AlbumModule(IArtistService artistService, + public AlbumController(IArtistService artistService, IAlbumService albumService, IAddAlbumService addAlbumService, IReleaseService releaseService, @@ -50,12 +52,6 @@ namespace Lidarr.Api.V1.Albums _releaseService = releaseService; _addAlbumService = addAlbumService; - GetResourceAll = GetAlbums; - CreateResource = AddAlbum; - UpdateResource = UpdateAlbum; - DeleteResource = DeleteAlbum; - Put("/monitor", x => SetAlbumsMonitored()); - PostValidator.RuleFor(s => s.ForeignAlbumId).NotEmpty(); PostValidator.RuleFor(s => s.Artist.QualityProfileId).SetValidator(qualityProfileExistsValidator); PostValidator.RuleFor(s => s.Artist.MetadataProfileId).SetValidator(metadataProfileExistsValidator); @@ -63,14 +59,13 @@ namespace Lidarr.Api.V1.Albums PostValidator.RuleFor(s => s.Artist.ForeignArtistId).NotEmpty(); } - private List GetAlbums() + [HttpGet] + public List GetAlbums([FromQuery]int? artistId, + [FromQuery] List albumIds, + [FromQuery]string foreignAlbumId, + [FromQuery]bool includeAllArtistAlbums = false) { - var artistIdQuery = Request.Query.ArtistId; - var albumIdsQuery = Request.Query.AlbumIds; - var foreignIdQuery = Request.Query.ForeignAlbumId; - var includeAllArtistAlbumsQuery = Request.Query.IncludeAllArtistAlbums; - - if (!Request.Query.ArtistId.HasValue && !albumIdsQuery.HasValue && !foreignIdQuery.HasValue) + if (!artistId.HasValue && !albumIds.Any() && foreignAlbumId.IsNullOrWhiteSpace()) { var albums = _albumService.GetAllAlbums(); @@ -93,17 +88,13 @@ namespace Lidarr.Api.V1.Albums return MapToResource(albums, false); } - if (artistIdQuery.HasValue) + if (artistId.HasValue) { - int artistId = Convert.ToInt32(artistIdQuery.Value); - - return MapToResource(_albumService.GetAlbumsByArtist(artistId), false); + return MapToResource(_albumService.GetAlbumsByArtist(artistId.Value), false); } - if (foreignIdQuery.HasValue) + if (foreignAlbumId.IsNotNullOrWhiteSpace()) { - string foreignAlbumId = foreignIdQuery.Value.ToString(); - var album = _albumService.FindById(foreignAlbumId); if (album == null) @@ -111,7 +102,7 @@ namespace Lidarr.Api.V1.Albums return MapToResource(new List(), false); } - if (includeAllArtistAlbumsQuery.HasValue && Convert.ToBoolean(includeAllArtistAlbumsQuery.Value)) + if (includeAllArtistAlbums) { return MapToResource(_albumService.GetAlbumsByArtist(album.ArtistId), false); } @@ -121,23 +112,19 @@ namespace Lidarr.Api.V1.Albums } } - string albumIdsValue = albumIdsQuery.Value.ToString(); - - var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => Convert.ToInt32(e)) - .ToList(); - return MapToResource(_albumService.GetAlbums(albumIds), false); } - private int AddAlbum(AlbumResource albumResource) + [RestPostById] + public ActionResult AddAlbum(AlbumResource albumResource) { var album = _addAlbumService.AddAlbum(albumResource.ToModel()); - return album.Id; + return Created(album.Id); } - private void UpdateAlbum(AlbumResource albumResource) + [RestPutById] + public ActionResult UpdateAlbum(AlbumResource albumResource) { var album = _albumService.GetAlbum(albumResource.Id); @@ -147,9 +134,12 @@ namespace Lidarr.Api.V1.Albums _releaseService.UpdateMany(model.AlbumReleases.Value); BroadcastResourceChange(ModelAction.Updated, model.Id); + + return Accepted(model.Id); } - private void DeleteAlbum(int id) + [RestDeleteById] + public void DeleteAlbum(int id) { var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion"); @@ -157,15 +147,15 @@ namespace Lidarr.Api.V1.Albums _albumService.DeleteAlbum(id, deleteFiles, addImportListExclusion); } - private object SetAlbumsMonitored() + [HttpPut("monitor")] + public IActionResult SetAlbumsMonitored([FromBody]AlbumsMonitoredResource resource) { - var resource = Request.Body.FromJson(); - _albumService.SetMonitored(resource.AlbumIds, resource.Monitored); - return ResponseWithCode(MapToResource(_albumService.GetAlbums(resource.AlbumIds), false), HttpStatusCode.Accepted); + return Accepted(MapToResource(_albumService.GetAlbums(resource.AlbumIds), false)); } + [NonAction] public void Handle(AlbumGrabbedEvent message) { foreach (var album in message.Album.Albums) @@ -177,31 +167,37 @@ namespace Lidarr.Api.V1.Albums } } + [NonAction] public void Handle(AlbumEditedEvent message) { BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true)); } + [NonAction] public void Handle(AlbumUpdatedEvent message) { BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true)); } + [NonAction] public void Handle(AlbumDeletedEvent message) { BroadcastResourceChange(ModelAction.Deleted, message.Album.ToResource()); } + [NonAction] public void Handle(AlbumImportedEvent message) { BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Album, true)); } + [NonAction] public void Handle(TrackImportedEvent message) { BroadcastResourceChange(ModelAction.Updated, message.TrackInfo.Album.ToResource()); } + [NonAction] public void Handle(TrackFileDeletedEvent message) { if (message.Reason == DeleteMediaFileReason.Upgrade) diff --git a/src/Lidarr.Api.V1/Albums/AlbumModuleWithSignalR.cs b/src/Lidarr.Api.V1/Albums/AlbumControllerWithSignalR.cs similarity index 78% rename from src/Lidarr.Api.V1/Albums/AlbumModuleWithSignalR.cs rename to src/Lidarr.Api.V1/Albums/AlbumControllerWithSignalR.cs index fa6d98859..50a197546 100644 --- a/src/Lidarr.Api.V1/Albums/AlbumModuleWithSignalR.cs +++ b/src/Lidarr.Api.V1/Albums/AlbumControllerWithSignalR.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; using Lidarr.Api.V1.Artist; -using Lidarr.Http; +using Lidarr.Http.REST; using NzbDrone.Common.Extensions; using NzbDrone.Core.ArtistStats; using NzbDrone.Core.DecisionEngine.Specifications; @@ -11,14 +11,14 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Albums { - public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR + public abstract class AlbumControllerWithSignalR : RestControllerWithSignalR { protected readonly IAlbumService _albumService; protected readonly IArtistStatisticsService _artistStatisticsService; protected readonly IUpgradableSpecification _qualityUpgradableSpecification; protected readonly IMapCoversToLocal _coverMapper; - protected AlbumModuleWithSignalR(IAlbumService albumService, + protected AlbumControllerWithSignalR(IAlbumService albumService, IArtistStatisticsService artistStatisticsService, IMapCoversToLocal coverMapper, IUpgradableSpecification qualityUpgradableSpecification, @@ -29,27 +29,9 @@ namespace Lidarr.Api.V1.Albums _artistStatisticsService = artistStatisticsService; _coverMapper = coverMapper; _qualityUpgradableSpecification = qualityUpgradableSpecification; - - GetResourceById = GetAlbum; - } - - protected AlbumModuleWithSignalR(IAlbumService albumService, - IArtistStatisticsService artistStatisticsService, - IMapCoversToLocal coverMapper, - IUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster, - string resource) - : base(signalRBroadcaster, resource) - { - _albumService = albumService; - _artistStatisticsService = artistStatisticsService; - _coverMapper = coverMapper; - _qualityUpgradableSpecification = qualityUpgradableSpecification; - - GetResourceById = GetAlbum; } - protected AlbumResource GetAlbum(int id) + public override AlbumResource GetResourceById(int id) { var album = _albumService.GetAlbum(id); var resource = MapToResource(album, true); diff --git a/src/Lidarr.Api.V1/Albums/AlbumLookupModule.cs b/src/Lidarr.Api.V1/Albums/AlbumLookupController.cs similarity index 72% rename from src/Lidarr.Api.V1/Albums/AlbumLookupModule.cs rename to src/Lidarr.Api.V1/Albums/AlbumLookupController.cs index 46d489836..99eb63193 100644 --- a/src/Lidarr.Api.V1/Albums/AlbumLookupModule.cs +++ b/src/Lidarr.Api.V1/Albums/AlbumLookupController.cs @@ -1,26 +1,26 @@ using System.Collections.Generic; using System.Linq; using Lidarr.Http; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; namespace Lidarr.Api.V1.Albums { - public class AlbumLookupModule : LidarrRestModule + [V1ApiController("album/lookup")] + public class AlbumLookupController : Controller { private readonly ISearchForNewAlbum _searchProxy; - public AlbumLookupModule(ISearchForNewAlbum searchProxy) - : base("/album/lookup") + public AlbumLookupController(ISearchForNewAlbum searchProxy) { _searchProxy = searchProxy; - Get("/", x => Search()); } - private object Search() + [HttpGet] + public object Search(string term) { - var searchResults = _searchProxy.SearchForNewAlbum((string)Request.Query.term, null); + var searchResults = _searchProxy.SearchForNewAlbum(term, null); return MapToResource(searchResults).ToList(); } diff --git a/src/Lidarr.Api.V1/Artist/ArtistModule.cs b/src/Lidarr.Api.V1/Artist/ArtistController.cs similarity index 91% rename from src/Lidarr.Api.V1/Artist/ArtistModule.cs rename to src/Lidarr.Api.V1/Artist/ArtistController.cs index 8b03027d8..58f3fea59 100644 --- a/src/Lidarr.Api.V1/Artist/ArtistModule.cs +++ b/src/Lidarr.Api.V1/Artist/ArtistController.cs @@ -4,6 +4,9 @@ using System.Linq; using FluentValidation; using Lidarr.Http; using Lidarr.Http.Extensions; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.ArtistStats; using NzbDrone.Core.Datastore.Events; @@ -22,7 +25,8 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Artist { - public class ArtistModule : LidarrRestModuleWithSignalR, + [V1ApiController] + public class ArtistController : RestControllerWithSignalR, IHandle, IHandle, IHandle, @@ -41,7 +45,7 @@ namespace Lidarr.Api.V1.Artist private readonly IManageCommandQueue _commandQueueManager; private readonly IRootFolderService _rootFolderService; - public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster, + public ArtistController(IBroadcastSignalRMessage signalRBroadcaster, IArtistService artistService, IAlbumService albumService, IAddArtistService addArtistService, @@ -68,12 +72,6 @@ namespace Lidarr.Api.V1.Artist _commandQueueManager = commandQueueManager; _rootFolderService = rootFolderService; - GetResourceAll = AllArtists; - GetResourceById = GetArtist; - CreateResource = AddArtist; - UpdateResource = UpdateArtist; - DeleteResource = DeleteArtist; - Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId)); Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.MetadataProfileId)); @@ -98,7 +96,7 @@ namespace Lidarr.Api.V1.Artist PutValidator.RuleFor(s => s.Path).IsValidPath(); } - private ArtistResource GetArtist(int id) + public override ArtistResource GetResourceById(int id) { var artist = _artistService.GetArtist(id); return GetArtistResource(artist); @@ -122,15 +120,15 @@ namespace Lidarr.Api.V1.Artist return resource; } - private List AllArtists() + [HttpGet] + public List AllArtists(Guid? mbId) { - var mbId = Request.GetGuidQueryParameter("mbId"); var artistStats = _artistStatisticsService.ArtistStatistics(); var artistsResources = new List(); - if (mbId != Guid.Empty) + if (mbId.HasValue) { - artistsResources.AddIfNotNull(_artistService.FindById(mbId.ToString()).ToResource()); + artistsResources.AddIfNotNull(_artistService.FindById(mbId.Value.ToString()).ToResource()); } else { @@ -146,14 +144,16 @@ namespace Lidarr.Api.V1.Artist return artistsResources; } - private int AddArtist(ArtistResource artistResource) + [RestPostById] + public ActionResult AddArtist(ArtistResource artistResource) { var artist = _addArtistService.AddArtist(artistResource.ToModel()); - return artist.Id; + return Created(artist.Id); } - private void UpdateArtist(ArtistResource artistResource) + [RestPutById] + public ActionResult UpdateArtist(ArtistResource artistResource) { var moveFiles = Request.GetBooleanQueryParameter("moveFiles"); var artist = _artistService.GetArtist(artistResource.Id); @@ -175,9 +175,12 @@ namespace Lidarr.Api.V1.Artist _artistService.UpdateArtist(model); BroadcastResourceChange(ModelAction.Updated, artistResource); + + return Accepted(artistResource.Id); } - private void DeleteArtist(int id) + [RestDeleteById] + public void DeleteArtist(int id) { var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion"); @@ -250,21 +253,25 @@ namespace Lidarr.Api.V1.Artist resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path); } + [NonAction] public void Handle(AlbumImportedEvent message) { BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist)); } + [NonAction] public void Handle(AlbumEditedEvent message) { BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Album.Artist.Value)); } + [NonAction] public void Handle(AlbumDeletedEvent message) { BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Album.Artist.Value)); } + [NonAction] public void Handle(TrackFileDeletedEvent message) { if (message.Reason == DeleteMediaFileReason.Upgrade) @@ -275,16 +282,19 @@ namespace Lidarr.Api.V1.Artist BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.TrackFile.Artist.Value)); } + [NonAction] public void Handle(ArtistUpdatedEvent message) { BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist)); } + [NonAction] public void Handle(ArtistEditedEvent message) { BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Artist)); } + [NonAction] public void Handle(ArtistsDeletedEvent message) { foreach (var artist in message.Artists) @@ -293,11 +303,13 @@ namespace Lidarr.Api.V1.Artist } } + [NonAction] public void Handle(ArtistRenamedEvent message) { BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); } + [NonAction] public void Handle(MediaCoversUpdatedEvent message) { if (message.Updated) diff --git a/src/Lidarr.Api.V1/Artist/ArtistEditorModule.cs b/src/Lidarr.Api.V1/Artist/ArtistEditorController.cs similarity index 78% rename from src/Lidarr.Api.V1/Artist/ArtistEditorModule.cs rename to src/Lidarr.Api.V1/Artist/ArtistEditorController.cs index ebf208aa8..99860c3f1 100644 --- a/src/Lidarr.Api.V1/Artist/ArtistEditorModule.cs +++ b/src/Lidarr.Api.V1/Artist/ArtistEditorController.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -using Lidarr.Http.Extensions; -using Nancy; +using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Music; @@ -9,23 +9,21 @@ using NzbDrone.Core.Music.Commands; namespace Lidarr.Api.V1.Artist { - public class ArtistEditorModule : LidarrV1Module + [V1ApiController("artist/editor")] + public class ArtistEditorController : Controller { private readonly IArtistService _artistService; private readonly IManageCommandQueue _commandQueueManager; - public ArtistEditorModule(IArtistService artistService, IManageCommandQueue commandQueueManager) - : base("/artist/editor") + public ArtistEditorController(IArtistService artistService, IManageCommandQueue commandQueueManager) { _artistService = artistService; _commandQueueManager = commandQueueManager; - Put("/", artist => SaveAll()); - Delete("/", artist => DeleteArtist()); } - private object SaveAll() + [HttpPut] + public IActionResult SaveAll([FromBody] ArtistEditorResource resource) { - var resource = Request.Body.FromJson(); var artistToUpdate = _artistService.GetArtists(resource.ArtistIds); var artistToMove = new List(); @@ -86,15 +84,12 @@ namespace Lidarr.Api.V1.Artist }); } - return ResponseWithCode(_artistService.UpdateArtists(artistToUpdate, !resource.MoveFiles) - .ToResource(), - HttpStatusCode.Accepted); + return Accepted(_artistService.UpdateArtists(artistToUpdate, !resource.MoveFiles).ToResource()); } - private object DeleteArtist() + [HttpDelete] + public object DeleteArtist([FromBody] ArtistEditorResource resource) { - var resource = Request.Body.FromJson(); - _artistService.DeleteArtists(resource.ArtistIds, resource.DeleteFiles); return new object(); diff --git a/src/Lidarr.Api.V1/Artist/ArtistImportModule.cs b/src/Lidarr.Api.V1/Artist/ArtistImportModule.cs deleted file mode 100644 index b2a50a0a7..000000000 --- a/src/Lidarr.Api.V1/Artist/ArtistImportModule.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using Lidarr.Http; -using Lidarr.Http.Extensions; -using Nancy; -using NzbDrone.Core.Music; - -namespace Lidarr.Api.V1.Artist -{ - public class ArtistImportModule : LidarrRestModule - { - private readonly IAddArtistService _addArtistService; - - public ArtistImportModule(IAddArtistService addArtistService) - : base("/artist/import") - { - _addArtistService = addArtistService; - Post("/", x => Import()); - } - - private object Import() - { - var resource = Request.Body.FromJson>(); - var newArtists = resource.ToModel(); - - return _addArtistService.AddArtists(newArtists).ToResource(); - } - } -} diff --git a/src/Lidarr.Api.V1/Artist/ArtistLookupModule.cs b/src/Lidarr.Api.V1/Artist/ArtistLookupController.cs similarity index 78% rename from src/Lidarr.Api.V1/Artist/ArtistLookupModule.cs rename to src/Lidarr.Api.V1/Artist/ArtistLookupController.cs index d435bb983..2c83a86df 100644 --- a/src/Lidarr.Api.V1/Artist/ArtistLookupModule.cs +++ b/src/Lidarr.Api.V1/Artist/ArtistLookupController.cs @@ -1,26 +1,26 @@ using System.Collections.Generic; using System.Linq; using Lidarr.Http; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; namespace Lidarr.Api.V1.Artist { - public class ArtistLookupModule : LidarrRestModule + [V1ApiController("artist/lookup")] + public class ArtistLookupController : Controller { private readonly ISearchForNewArtist _searchProxy; - public ArtistLookupModule(ISearchForNewArtist searchProxy) - : base("/artist/lookup") + public ArtistLookupController(ISearchForNewArtist searchProxy) { _searchProxy = searchProxy; - Get("/", x => Search()); } - private object Search() + [HttpGet] + public object Search([FromQuery] string term) { - var searchResults = _searchProxy.SearchForNewArtist((string)Request.Query.term); + var searchResults = _searchProxy.SearchForNewArtist(term); return MapToResource(searchResults).ToList(); } diff --git a/src/Lidarr.Api.V1/Blacklist/BlacklistController.cs b/src/Lidarr.Api.V1/Blacklist/BlacklistController.cs new file mode 100644 index 000000000..ca19731c9 --- /dev/null +++ b/src/Lidarr.Api.V1/Blacklist/BlacklistController.cs @@ -0,0 +1,43 @@ +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Datastore; + +namespace Lidarr.Api.V1.Blacklist +{ + [V1ApiController] + public class BlacklistController : Controller + { + private readonly IBlacklistService _blacklistService; + + public BlacklistController(IBlacklistService blacklistService) + { + _blacklistService = blacklistService; + } + + [HttpGet] + public PagingResource GetBlacklist() + { + var pagingResource = Request.ReadPagingResourceFromRequest(); + var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); + + return pagingSpec.ApplyToPage(_blacklistService.Paged, BlacklistResourceMapper.MapToResource); + } + + [RestDeleteById] + public void DeleteBlacklist(int id) + { + _blacklistService.Delete(id); + } + + [HttpDelete("bulk")] + public object Remove([FromBody] BlacklistBulkResource resource) + { + _blacklistService.Delete(resource.Ids); + + return new object(); + } + } +} diff --git a/src/Lidarr.Api.V1/Blacklist/BlacklistModule.cs b/src/Lidarr.Api.V1/Blacklist/BlacklistModule.cs deleted file mode 100644 index 1b1019d1b..000000000 --- a/src/Lidarr.Api.V1/Blacklist/BlacklistModule.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Lidarr.Http; -using Lidarr.Http.Extensions; -using NzbDrone.Core.Blacklisting; -using NzbDrone.Core.Datastore; - -namespace Lidarr.Api.V1.Blacklist -{ - public class BlacklistModule : LidarrRestModule - { - private readonly IBlacklistService _blacklistService; - - public BlacklistModule(IBlacklistService blacklistService) - { - _blacklistService = blacklistService; - GetResourcePaged = GetBlacklist; - DeleteResource = DeleteBlacklist; - - Delete("/bulk", x => Remove()); - } - - private PagingResource GetBlacklist(PagingResource pagingResource) - { - var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); - - return ApplyToPage(_blacklistService.Paged, pagingSpec, BlacklistResourceMapper.MapToResource); - } - - private void DeleteBlacklist(int id) - { - _blacklistService.Delete(id); - } - - private object Remove() - { - var resource = Request.Body.FromJson(); - - _blacklistService.Delete(resource.Ids); - - return new object(); - } - } -} diff --git a/src/Lidarr.Api.V1/Calendar/CalendarController.cs b/src/Lidarr.Api.V1/Calendar/CalendarController.cs new file mode 100644 index 000000000..af29f0f31 --- /dev/null +++ b/src/Lidarr.Api.V1/Calendar/CalendarController.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Lidarr.Api.V1.Albums; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.ArtistStats; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; + +namespace Lidarr.Api.V1.Calendar +{ + [V1ApiController] + public class CalendarController : AlbumControllerWithSignalR + { + public CalendarController(IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IMapCoversToLocal coverMapper, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster) + { + } + + [HttpGet] + public List GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeArtist = false) + { + //TODO: Add Album Image support to AlbumControllerWithSignalR + var includeAlbumImages = Request.GetBooleanQueryParameter("includeAlbumImages"); + + var startUse = start ?? DateTime.Today; + var endUse = end ?? DateTime.Today.AddDays(2); + + var resources = MapToResource(_albumService.AlbumsBetweenDates(startUse, endUse, unmonitored), includeArtist); + + return resources.OrderBy(e => e.ReleaseDate).ToList(); + } + } +} diff --git a/src/Lidarr.Api.V1/Calendar/CalendarFeedModule.cs b/src/Lidarr.Api.V1/Calendar/CalendarFeedController.cs similarity index 64% rename from src/Lidarr.Api.V1/Calendar/CalendarFeedModule.cs rename to src/Lidarr.Api.V1/Calendar/CalendarFeedController.cs index 3ee421434..89159a1fc 100644 --- a/src/Lidarr.Api.V1/Calendar/CalendarFeedModule.cs +++ b/src/Lidarr.Api.V1/Calendar/CalendarFeedController.cs @@ -5,60 +5,38 @@ using Ical.Net; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using Ical.Net.Serialization; -using Lidarr.Http.Extensions; -using Nancy; -using Nancy.Responses; +using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Music; using NzbDrone.Core.Tags; namespace Lidarr.Api.V1.Calendar { - public class CalendarFeedModule : LidarrV1FeedModule + [V1FeedController("calendar")] + public class CalendarFeedController : Controller { private readonly IAlbumService _albumService; private readonly IArtistService _artistService; private readonly ITagService _tagService; - public CalendarFeedModule(IAlbumService albumService, IArtistService artistService, ITagService tagService) - : base("calendar") + public CalendarFeedController(IAlbumService albumService, IArtistService artistService, ITagService tagService) { _albumService = albumService; _artistService = artistService; _tagService = tagService; - - Get("/Lidarr.ics", options => GetCalendarFeed()); } - private object GetCalendarFeed() + [HttpGet("Lidarr.ics")] + public IActionResult GetCalendarFeed(int pastDays = 7, int futureDays = 28, string tagList = "", bool unmonitored = false) { - var pastDays = 7; - var futureDays = 28; var start = DateTime.Today.AddDays(-pastDays); var end = DateTime.Today.AddDays(futureDays); - var unmonitored = Request.GetBooleanQueryParameter("unmonitored"); var tags = new List(); - var queryPastDays = Request.Query.PastDays; - var queryFutureDays = Request.Query.FutureDays; - var queryTags = Request.Query.Tags; - - if (queryPastDays.HasValue) - { - pastDays = int.Parse(queryPastDays.Value); - start = DateTime.Today.AddDays(-pastDays); - } - - if (queryFutureDays.HasValue) - { - futureDays = int.Parse(queryFutureDays.Value); - end = DateTime.Today.AddDays(futureDays); - } - - if (queryTags.HasValue) + if (tagList.IsNotNullOrWhiteSpace()) { - var tagInput = (string)queryTags.Value.ToString(); - tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id)); + tags.AddRange(tagList.Split(',').Select(_tagService.GetTag).Select(t => t.Id)); } var albums = _albumService.AlbumsBetweenDates(start, end, unmonitored); @@ -95,7 +73,7 @@ namespace Lidarr.Api.V1.Calendar var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext()); var icalendar = serializer.SerializeToString(calendar); - return new TextResponse(icalendar, "text/calendar"); + return Content(icalendar, "text/calendar"); } } } diff --git a/src/Lidarr.Api.V1/Calendar/CalendarModule.cs b/src/Lidarr.Api.V1/Calendar/CalendarModule.cs deleted file mode 100644 index 410e38e82..000000000 --- a/src/Lidarr.Api.V1/Calendar/CalendarModule.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Lidarr.Api.V1.Albums; -using Lidarr.Http.Extensions; -using NzbDrone.Core.ArtistStats; -using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.Music; -using NzbDrone.SignalR; - -namespace Lidarr.Api.V1.Calendar -{ - public class CalendarModule : AlbumModuleWithSignalR - { - public CalendarModule(IAlbumService albumService, - IArtistStatisticsService artistStatisticsService, - IMapCoversToLocal coverMapper, - IUpgradableSpecification upgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster, "calendar") - { - GetResourceAll = GetCalendar; - } - - private List GetCalendar() - { - var start = DateTime.Today; - var end = DateTime.Today.AddDays(2); - var includeUnmonitored = Request.GetBooleanQueryParameter("unmonitored"); - var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); - - //TODO: Add Album Image support to AlbumModuleWithSignalR - var includeAlbumImages = Request.GetBooleanQueryParameter("includeAlbumImages"); - - var queryStart = Request.Query.Start; - var queryEnd = Request.Query.End; - - if (queryStart.HasValue) - { - start = DateTime.Parse(queryStart.Value); - } - - if (queryEnd.HasValue) - { - end = DateTime.Parse(queryEnd.Value); - } - - var resources = MapToResource(_albumService.AlbumsBetweenDates(start, end, includeUnmonitored), includeArtist); - - return resources.OrderBy(e => e.ReleaseDate).ToList(); - } - } -} diff --git a/src/Lidarr.Api.V1/Commands/CommandModule.cs b/src/Lidarr.Api.V1/Commands/CommandController.cs similarity index 66% rename from src/Lidarr.Api.V1/Commands/CommandModule.cs rename to src/Lidarr.Api.V1/Commands/CommandController.cs index 35c0e80f4..331a52d01 100644 --- a/src/Lidarr.Api.V1/Commands/CommandModule.cs +++ b/src/Lidarr.Api.V1/Commands/CommandController.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Lidarr.Http; -using Lidarr.Http.Extensions; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; using Lidarr.Http.Validation; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common; +using NzbDrone.Common.Serializer; using NzbDrone.Common.TPL; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Commands; @@ -14,14 +18,15 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Commands { - public class CommandModule : LidarrRestModuleWithSignalR, IHandle + [V1ApiController] + public class CommandController : RestControllerWithSignalR, IHandle { private readonly IManageCommandQueue _commandQueueManager; private readonly IServiceFactory _serviceFactory; private readonly Debouncer _debouncer; private readonly Dictionary _pendingUpdates; - public CommandModule(IManageCommandQueue commandQueueManager, + public CommandController(IManageCommandQueue commandQueueManager, IBroadcastSignalRMessage signalRBroadcaster, IServiceFactory serviceFactory) : base(signalRBroadcaster) @@ -29,50 +34,55 @@ namespace Lidarr.Api.V1.Commands _commandQueueManager = commandQueueManager; _serviceFactory = serviceFactory; - GetResourceById = GetCommand; - CreateResource = StartCommand; - GetResourceAll = GetStartedCommands; - DeleteResource = CancelCommand; - PostValidator.RuleFor(c => c.Name).NotBlank(); _debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1)); _pendingUpdates = new Dictionary(); } - private CommandResource GetCommand(int id) + public override CommandResource GetResourceById(int id) { return _commandQueueManager.Get(id).ToResource(); } - private int StartCommand(CommandResource commandResource) + [RestPostById] + public ActionResult StartCommand(CommandResource commandResource) { var commandType = _serviceFactory.GetImplementations(typeof(Command)) .Single(c => c.Name.Replace("Command", "") .Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase)); - dynamic command = Request.Body.FromJson(commandType); - command.Trigger = CommandTrigger.Manual; - command.SuppressMessages = !command.SendUpdatesToClient; - command.SendUpdatesToClient = true; + Request.Body.Seek(0, SeekOrigin.Begin); + using (var reader = new StreamReader(Request.Body)) + { + var body = reader.ReadToEnd(); + + dynamic command = STJson.Deserialize(body, commandType); - command.ClientUserAgent = Request.Headers.UserAgent; + 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 trackedCommand.Id; + var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); + return Created(trackedCommand.Id); + } } - private List GetStartedCommands() + [HttpGet] + public List GetStartedCommands() { return _commandQueueManager.All().ToResource(); } - private void CancelCommand(int id) + [RestDeleteById] + public void CancelCommand(int id) { _commandQueueManager.Cancel(id); } + [NonAction] public void Handle(CommandUpdatedEvent message) { if (message.Command.Body.SendUpdatesToClient) diff --git a/src/Lidarr.Api.V1/Config/ConfigController.cs b/src/Lidarr.Api.V1/Config/ConfigController.cs new file mode 100644 index 000000000..8f1fb19f4 --- /dev/null +++ b/src/Lidarr.Api.V1/Config/ConfigController.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Reflection; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Api.V1.Config +{ + public abstract class ConfigController : RestController + where TResource : RestResource, new() + { + private readonly IConfigService _configService; + + protected ConfigController(IConfigService configService) + { + _configService = configService; + } + + public override TResource GetResourceById(int id) + { + return GetConfig(); + } + + [HttpGet] + public TResource GetConfig() + { + var resource = ToResource(_configService); + resource.Id = 1; + + return resource; + } + + [RestPutById] + public 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/Lidarr.Api.V1/Config/DownloadClientConfigModule.cs b/src/Lidarr.Api.V1/Config/DownloadClientConfigController.cs similarity index 57% rename from src/Lidarr.Api.V1/Config/DownloadClientConfigModule.cs rename to src/Lidarr.Api.V1/Config/DownloadClientConfigController.cs index 38df2d62a..c63396dbf 100644 --- a/src/Lidarr.Api.V1/Config/DownloadClientConfigModule.cs +++ b/src/Lidarr.Api.V1/Config/DownloadClientConfigController.cs @@ -1,10 +1,12 @@ +using Lidarr.Http; using NzbDrone.Core.Configuration; namespace Lidarr.Api.V1.Config { - public class DownloadClientConfigModule : LidarrConfigModule + [V1ApiController("config/downloadclient")] + public class DownloadClientConfigController : ConfigController { - public DownloadClientConfigModule(IConfigService configService) + public DownloadClientConfigController(IConfigService configService) : base(configService) { } diff --git a/src/Lidarr.Api.V1/Config/HostConfigModule.cs b/src/Lidarr.Api.V1/Config/HostConfigController.cs similarity index 83% rename from src/Lidarr.Api.V1/Config/HostConfigModule.cs rename to src/Lidarr.Api.V1/Config/HostConfigController.cs index c3311479d..ce490fd96 100644 --- a/src/Lidarr.Api.V1/Config/HostConfigModule.cs +++ b/src/Lidarr.Api.V1/Config/HostConfigController.cs @@ -4,6 +4,9 @@ using System.Reflection; using System.Security.Cryptography.X509Certificates; using FluentValidation; using Lidarr.Http; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; @@ -13,26 +16,22 @@ using NzbDrone.Core.Validation.Paths; namespace Lidarr.Api.V1.Config { - public class HostConfigModule : LidarrRestModule + [V1ApiController("config/host")] + public class HostConfigController : RestController { private readonly IConfigFileProvider _configFileProvider; private readonly IConfigService _configService; private readonly IUserService _userService; - public HostConfigModule(IConfigFileProvider configFileProvider, - IConfigService configService, - IUserService userService, - FileExistsValidator fileExistsValidator) - : base("/config/host") + public HostConfigController(IConfigFileProvider configFileProvider, + IConfigService configService, + IUserService userService, + FileExistsValidator fileExistsValidator) { _configFileProvider = configFileProvider; _configService = configService; _userService = userService; - GetResourceSingle = GetHostConfig; - GetResourceById = GetHostConfig; - UpdateResource = SaveHostConfig; - SharedValidator.RuleFor(c => c.BindAddress) .ValidIp4Address() .NotListenAllIp4Address() @@ -79,7 +78,13 @@ namespace Lidarr.Api.V1.Config return cert != null; } - private HostConfigResource GetHostConfig() + public override HostConfigResource GetResourceById(int id) + { + return GetHostConfig(); + } + + [HttpGet] + public HostConfigResource GetHostConfig() { var resource = _configFileProvider.ToResource(_configService); resource.Id = 1; @@ -94,12 +99,8 @@ namespace Lidarr.Api.V1.Config return resource; } - private HostConfigResource GetHostConfig(int id) - { - return GetHostConfig(); - } - - private void SaveHostConfig(HostConfigResource resource) + [RestPutById] + public ActionResult SaveHostConfig(HostConfigResource resource) { var dictionary = resource.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) @@ -112,6 +113,8 @@ namespace Lidarr.Api.V1.Config { _userService.Upsert(resource.Username, resource.Password); } + + return Accepted(resource.Id); } } } diff --git a/src/Lidarr.Api.V1/Config/IndexerConfigModule.cs b/src/Lidarr.Api.V1/Config/IndexerConfigController.cs similarity index 79% rename from src/Lidarr.Api.V1/Config/IndexerConfigModule.cs rename to src/Lidarr.Api.V1/Config/IndexerConfigController.cs index 7b0ed8d97..81db83864 100644 --- a/src/Lidarr.Api.V1/Config/IndexerConfigModule.cs +++ b/src/Lidarr.Api.V1/Config/IndexerConfigController.cs @@ -1,12 +1,14 @@ using FluentValidation; +using Lidarr.Http; using Lidarr.Http.Validation; using NzbDrone.Core.Configuration; namespace Lidarr.Api.V1.Config { - public class IndexerConfigModule : LidarrConfigModule + [V1ApiController("config/indexer")] + public class IndexerConfigController : ConfigController { - public IndexerConfigModule(IConfigService configService) + public IndexerConfigController(IConfigService configService) : base(configService) { SharedValidator.RuleFor(c => c.MinimumAge) diff --git a/src/Lidarr.Api.V1/Config/LidarrConfigModule.cs b/src/Lidarr.Api.V1/Config/LidarrConfigModule.cs deleted file mode 100644 index cf2798215..000000000 --- a/src/Lidarr.Api.V1/Config/LidarrConfigModule.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Linq; -using System.Reflection; -using Lidarr.Http; -using Lidarr.Http.REST; -using NzbDrone.Core.Configuration; - -namespace Lidarr.Api.V1.Config -{ - public abstract class LidarrConfigModule : LidarrRestModule - where TResource : RestResource, new() - { - private readonly IConfigService _configService; - - protected LidarrConfigModule(IConfigService configService) - : this(new TResource().ResourceName.Replace("config", ""), configService) - { - } - - protected LidarrConfigModule(string resource, IConfigService configService) - : base("config/" + resource.Trim('/')) - { - _configService = configService; - - GetResourceSingle = GetConfig; - GetResourceById = GetConfig; - UpdateResource = SaveConfig; - } - - private TResource GetConfig() - { - var resource = ToResource(_configService); - resource.Id = 1; - - return resource; - } - - protected abstract TResource ToResource(IConfigService model); - - private TResource GetConfig(int id) - { - return GetConfig(); - } - - private void SaveConfig(TResource resource) - { - var dictionary = resource.GetType() - .GetProperties(BindingFlags.Instance | BindingFlags.Public) - .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); - - _configService.SaveConfigDictionary(dictionary); - } - } -} diff --git a/src/Lidarr.Api.V1/Config/MediaManagementConfigModule.cs b/src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs similarity index 74% rename from src/Lidarr.Api.V1/Config/MediaManagementConfigModule.cs rename to src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs index 1986bd61e..d2acc26a8 100644 --- a/src/Lidarr.Api.V1/Config/MediaManagementConfigModule.cs +++ b/src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs @@ -1,4 +1,5 @@ using FluentValidation; +using Lidarr.Http; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; using NzbDrone.Core.Validation; @@ -6,9 +7,10 @@ using NzbDrone.Core.Validation.Paths; namespace Lidarr.Api.V1.Config { - public class MediaManagementConfigModule : LidarrConfigModule + [V1ApiController("config/mediamanagement")] + public class MediaManagementConfigController : ConfigController { - public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator) + public MediaManagementConfigController(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator) : base(configService) { SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0); diff --git a/src/Lidarr.Api.V1/Config/MetadataProviderConfigModule.cs b/src/Lidarr.Api.V1/Config/MetadataProviderConfigController.cs similarity index 68% rename from src/Lidarr.Api.V1/Config/MetadataProviderConfigModule.cs rename to src/Lidarr.Api.V1/Config/MetadataProviderConfigController.cs index e4c0db6d2..c6dc25635 100644 --- a/src/Lidarr.Api.V1/Config/MetadataProviderConfigModule.cs +++ b/src/Lidarr.Api.V1/Config/MetadataProviderConfigController.cs @@ -1,13 +1,15 @@ using FluentValidation; +using Lidarr.Http; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Validation; namespace Lidarr.Api.V1.Config { - public class MetadataProviderConfigModule : LidarrConfigModule + [V1ApiController("config/metadataprovider")] + public class MetadataProviderConfigController : ConfigController { - public MetadataProviderConfigModule(IConfigService configService) + public MetadataProviderConfigController(IConfigService configService) : base(configService) { SharedValidator.RuleFor(c => c.MetadataSource).IsValidUrl().When(c => !c.MetadataSource.IsNullOrWhiteSpace()); diff --git a/src/Lidarr.Api.V1/Config/NamingConfigModule.cs b/src/Lidarr.Api.V1/Config/NamingConfigController.cs similarity index 79% rename from src/Lidarr.Api.V1/Config/NamingConfigModule.cs rename to src/Lidarr.Api.V1/Config/NamingConfigController.cs index b4d77fd8b..ff1a6b52e 100644 --- a/src/Lidarr.Api.V1/Config/NamingConfigModule.cs +++ b/src/Lidarr.Api.V1/Config/NamingConfigController.cs @@ -3,49 +3,44 @@ using System.Linq; using FluentValidation; using FluentValidation.Results; using Lidarr.Http; -using Nancy.ModelBinding; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Organizer; namespace Lidarr.Api.V1.Config { - public class NamingConfigModule : LidarrRestModule + [V1ApiController("config/naming")] + public class NamingConfigController : RestController { private readonly INamingConfigService _namingConfigService; private readonly IFilenameSampleService _filenameSampleService; private readonly IFilenameValidationService _filenameValidationService; private readonly IBuildFileNames _filenameBuilder; - public NamingConfigModule(INamingConfigService namingConfigService, - IFilenameSampleService filenameSampleService, - IFilenameValidationService filenameValidationService, - IBuildFileNames filenameBuilder) - : base("config/naming") + public NamingConfigController(INamingConfigService namingConfigService, + IFilenameSampleService filenameSampleService, + IFilenameValidationService filenameValidationService, + IBuildFileNames filenameBuilder) { _namingConfigService = namingConfigService; _filenameSampleService = filenameSampleService; _filenameValidationService = filenameValidationService; _filenameBuilder = filenameBuilder; - GetResourceSingle = GetNamingConfig; - GetResourceById = GetNamingConfig; - UpdateResource = UpdateNamingConfig; - - Get("/examples", x => GetExamples(this.Bind())); SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidTrackFormat(); SharedValidator.RuleFor(c => c.MultiDiscTrackFormat).ValidTrackFormat(); SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat(); } - private void UpdateNamingConfig(NamingConfigResource resource) + public override NamingConfigResource GetResourceById(int id) { - var nameSpec = resource.ToModel(); - ValidateFormatResult(nameSpec); - - _namingConfigService.Save(nameSpec); + return GetNamingConfig(); } - private NamingConfigResource GetNamingConfig() + [HttpGet] + public NamingConfigResource GetNamingConfig() { var nameSpec = _namingConfigService.GetConfig(); var resource = nameSpec.ToResource(); @@ -65,12 +60,19 @@ namespace Lidarr.Api.V1.Config return resource; } - private NamingConfigResource GetNamingConfig(int id) + [RestPutById] + public ActionResult UpdateNamingConfig(NamingConfigResource resource) { - return GetNamingConfig(); + var nameSpec = resource.ToModel(); + ValidateFormatResult(nameSpec); + + _namingConfigService.Save(nameSpec); + + return Accepted(resource.Id); } - private object GetExamples(NamingConfigResource config) + [HttpGet("examples")] + public object GetExamples([FromQuery]NamingConfigResource config) { if (config.Id == 0) { diff --git a/src/Lidarr.Api.V1/Config/UiConfigModule.cs b/src/Lidarr.Api.V1/Config/UiConfigController.cs similarity index 53% rename from src/Lidarr.Api.V1/Config/UiConfigModule.cs rename to src/Lidarr.Api.V1/Config/UiConfigController.cs index f25ba424d..fbeb2016a 100644 --- a/src/Lidarr.Api.V1/Config/UiConfigModule.cs +++ b/src/Lidarr.Api.V1/Config/UiConfigController.cs @@ -1,10 +1,12 @@ -using NzbDrone.Core.Configuration; +using Lidarr.Http; +using NzbDrone.Core.Configuration; namespace Lidarr.Api.V1.Config { - public class UiConfigModule : LidarrConfigModule + [V1ApiController("config/ui")] + public class UiConfigController : ConfigController { - public UiConfigModule(IConfigService configService) + public UiConfigController(IConfigService configService) : base(configService) { } diff --git a/src/Lidarr.Api.V1/CustomFilters/CustomFilterController.cs b/src/Lidarr.Api.V1/CustomFilters/CustomFilterController.cs new file mode 100644 index 000000000..7d17175d8 --- /dev/null +++ b/src/Lidarr.Api.V1/CustomFilters/CustomFilterController.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Lidarr.Http; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.CustomFilters; + +namespace Lidarr.Api.V1.CustomFilters +{ + [V1ApiController] + public class CustomFilterController : RestController + { + private readonly ICustomFilterService _customFilterService; + + public CustomFilterController(ICustomFilterService customFilterService) + { + _customFilterService = customFilterService; + } + + public 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/Lidarr.Api.V1/CustomFilters/CustomFilterModule.cs b/src/Lidarr.Api.V1/CustomFilters/CustomFilterModule.cs deleted file mode 100644 index a483b8d9c..000000000 --- a/src/Lidarr.Api.V1/CustomFilters/CustomFilterModule.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using Lidarr.Http; -using NzbDrone.Core.CustomFilters; - -namespace Lidarr.Api.V1.CustomFilters -{ - public class CustomFilterModule : LidarrRestModule - { - private readonly ICustomFilterService _customFilterService; - - public CustomFilterModule(ICustomFilterService customFilterService) - { - _customFilterService = customFilterService; - - GetResourceById = GetCustomFilter; - GetResourceAll = GetCustomFilters; - CreateResource = AddCustomFilter; - UpdateResource = UpdateCustomFilter; - DeleteResource = DeleteCustomResource; - } - - private CustomFilterResource GetCustomFilter(int id) - { - return _customFilterService.Get(id).ToResource(); - } - - private List GetCustomFilters() - { - return _customFilterService.All().ToResource(); - } - - private int AddCustomFilter(CustomFilterResource resource) - { - var customFilter = _customFilterService.Add(resource.ToModel()); - - return customFilter.Id; - } - - private void UpdateCustomFilter(CustomFilterResource resource) - { - _customFilterService.Update(resource.ToModel()); - } - - private void DeleteCustomResource(int id) - { - _customFilterService.Delete(id); - } - } -} diff --git a/src/Lidarr.Api.V1/DiskSpace/DiskSpaceModule.cs b/src/Lidarr.Api.V1/DiskSpace/DiskSpaceController.cs similarity index 62% rename from src/Lidarr.Api.V1/DiskSpace/DiskSpaceModule.cs rename to src/Lidarr.Api.V1/DiskSpace/DiskSpaceController.cs index 50a7539d4..b4b579638 100644 --- a/src/Lidarr.Api.V1/DiskSpace/DiskSpaceModule.cs +++ b/src/Lidarr.Api.V1/DiskSpace/DiskSpaceController.cs @@ -1,20 +1,21 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.DiskSpace; namespace Lidarr.Api.V1.DiskSpace { - public class DiskSpaceModule : LidarrRestModule + [V1ApiController("diskspace")] + public class DiskSpaceController : Controller { private readonly IDiskSpaceService _diskSpaceService; - public DiskSpaceModule(IDiskSpaceService diskSpaceService) - : base("diskspace") + public DiskSpaceController(IDiskSpaceService diskSpaceService) { _diskSpaceService = diskSpaceService; - GetResourceAll = GetFreeSpace; } + [HttpGet] public List GetFreeSpace() { return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource); diff --git a/src/Lidarr.Api.V1/DownloadClient/DownloadClientModule.cs b/src/Lidarr.Api.V1/DownloadClient/DownloadClientController.cs similarity index 64% rename from src/Lidarr.Api.V1/DownloadClient/DownloadClientModule.cs rename to src/Lidarr.Api.V1/DownloadClient/DownloadClientController.cs index e251e124b..61af12912 100644 --- a/src/Lidarr.Api.V1/DownloadClient/DownloadClientModule.cs +++ b/src/Lidarr.Api.V1/DownloadClient/DownloadClientController.cs @@ -1,12 +1,14 @@ -using NzbDrone.Core.Download; +using Lidarr.Http; +using NzbDrone.Core.Download; namespace Lidarr.Api.V1.DownloadClient { - public class DownloadClientModule : ProviderModuleBase + [V1ApiController] + public class DownloadClientController : ProviderControllerBase { public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper(); - public DownloadClientModule(IDownloadClientFactory downloadClientFactory) + public DownloadClientController(IDownloadClientFactory downloadClientFactory) : base(downloadClientFactory, "downloadclient", ResourceMapper) { } diff --git a/src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs b/src/Lidarr.Api.V1/FileSystem/FileSystemController.cs similarity index 54% rename from src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs rename to src/Lidarr.Api.V1/FileSystem/FileSystemController.cs index 14466f5a8..948d3b76b 100644 --- a/src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs +++ b/src/Lidarr.Api.V1/FileSystem/FileSystemController.cs @@ -1,44 +1,36 @@ using System.Linq; -using Lidarr.Http.Extensions; -using Nancy; +using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Disk; using NzbDrone.Core.MediaFiles; namespace Lidarr.Api.V1.FileSystem { - public class FileSystemModule : LidarrV1Module + [V1ApiController] + public class FileSystemController : Controller { private readonly IFileSystemLookupService _fileSystemLookupService; private readonly IDiskProvider _diskProvider; private readonly IDiskScanService _diskScanService; - public FileSystemModule(IFileSystemLookupService fileSystemLookupService, + public FileSystemController(IFileSystemLookupService fileSystemLookupService, IDiskProvider diskProvider, IDiskScanService diskScanService) - : base("/filesystem") { _fileSystemLookupService = fileSystemLookupService; _diskProvider = diskProvider; _diskScanService = diskScanService; - Get("/", x => GetContents()); - Get("/type", x => GetEntityType()); - Get("/mediafiles", x => GetMediaFiles()); } - private object GetContents() + [HttpGet] + public IActionResult GetContents(string path, bool includeFiles = false, bool allowFoldersWithoutTrailingSlashes = false) { - var pathQuery = Request.Query.path; - var includeFiles = Request.GetBooleanQueryParameter("includeFiles"); - var allowFoldersWithoutTrailingSlashes = Request.GetBooleanQueryParameter("allowFoldersWithoutTrailingSlashes"); - - return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles, allowFoldersWithoutTrailingSlashes); + return Ok(_fileSystemLookupService.LookupContents(path, includeFiles, allowFoldersWithoutTrailingSlashes)); } - private object GetEntityType() + [HttpGet("type")] + public object GetEntityType(string path) { - var pathQuery = Request.Query.path; - var path = (string)pathQuery.Value; - if (_diskProvider.FileExists(path)) { return new { type = "file" }; @@ -48,11 +40,9 @@ namespace Lidarr.Api.V1.FileSystem return new { type = "folder" }; } - private object GetMediaFiles() + [HttpGet("mediafiles")] + public object GetMediaFiles(string path) { - var pathQuery = Request.Query.path; - var path = (string)pathQuery.Value; - if (!_diskProvider.FolderExists(path)) { return new string[0]; diff --git a/src/Lidarr.Api.V1/Health/HealthModule.cs b/src/Lidarr.Api.V1/Health/HealthController.cs similarity index 54% rename from src/Lidarr.Api.V1/Health/HealthModule.cs rename to src/Lidarr.Api.V1/Health/HealthController.cs index e75058004..cd83893e6 100644 --- a/src/Lidarr.Api.V1/Health/HealthModule.cs +++ b/src/Lidarr.Api.V1/Health/HealthController.cs @@ -1,5 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Lidarr.Http; +using Lidarr.Http.REST; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.HealthCheck; using NzbDrone.Core.Messaging.Events; @@ -7,23 +10,30 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Health { - public class HealthModule : LidarrRestModuleWithSignalR, + [V1ApiController] + public class HealthController : RestControllerWithSignalR, IHandle { private readonly IHealthCheckService _healthCheckService; - public HealthModule(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService) + public HealthController(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService) : base(signalRBroadcaster) { _healthCheckService = healthCheckService; - GetResourceAll = GetHealth; } - private List GetHealth() + public 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/Lidarr.Api.V1/History/HistoryModule.cs b/src/Lidarr.Api.V1/History/HistoryController.cs similarity index 53% rename from src/Lidarr.Api.V1/History/HistoryModule.cs rename to src/Lidarr.Api.V1/History/HistoryController.cs index 6ec0e31cf..a287c6dff 100644 --- a/src/Lidarr.Api.V1/History/HistoryModule.cs +++ b/src/Lidarr.Api.V1/History/HistoryController.cs @@ -6,8 +6,7 @@ using Lidarr.Api.V1.Artist; using Lidarr.Api.V1.Tracks; using Lidarr.Http; using Lidarr.Http.Extensions; -using Lidarr.Http.REST; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; @@ -15,24 +14,20 @@ using NzbDrone.Core.History; namespace Lidarr.Api.V1.History { - public class HistoryModule : LidarrRestModule + [V1ApiController] + public class HistoryController : Controller { private readonly IHistoryService _historyService; private readonly IUpgradableSpecification _upgradableSpecification; private readonly IFailedDownloadService _failedDownloadService; - public HistoryModule(IHistoryService historyService, + public HistoryController(IHistoryService historyService, IUpgradableSpecification upgradableSpecification, IFailedDownloadService failedDownloadService) { _historyService = historyService; _upgradableSpecification = upgradableSpecification; _failedDownloadService = failedDownloadService; - GetResourcePaged = GetHistory; - - Get("/since", x => GetHistorySince()); - Get("/artist", x => GetArtistHistory()); - Post("/failed", x => MarkAsFailed()); } protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeArtist, bool includeAlbum, bool includeTrack) @@ -62,12 +57,11 @@ namespace Lidarr.Api.V1.History return resource; } - private PagingResource GetHistory(PagingResource pagingResource) + [HttpGet] + public PagingResource GetHistory(bool includeArtist = false, bool includeAlbum = false, bool includeTrack = false) { + var pagingResource = Request.ReadPagingResourceFromRequest(); var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); - var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); - var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum"); - var includeTrack = Request.GetBooleanQueryParameter("includeTrack"); var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType"); var albumIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "albumId"); @@ -91,68 +85,29 @@ namespace Lidarr.Api.V1.History pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId); } - return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeArtist, includeAlbum, includeTrack)); + return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h, includeArtist, includeAlbum, includeTrack)); } - private List GetHistorySince() + [HttpGet("since")] + public List GetHistorySince(DateTime date, HistoryEventType? eventType = null, bool includeArtist = false, bool includeAlbum = false, bool includeTrack = false) { - var queryDate = Request.Query.Date; - var queryEventType = Request.Query.EventType; - - if (!queryDate.HasValue) - { - throw new BadRequestException("date is missing"); - } - - DateTime date = DateTime.Parse(queryDate.Value); - HistoryEventType? eventType = null; - var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); - var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum"); - var includeTrack = Request.GetBooleanQueryParameter("includeTrack"); - - if (queryEventType.HasValue) - { - eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value); - } - return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList(); } - private List GetArtistHistory() + [HttpGet("artist")] + public List GetArtistHistory(int artistId, int? albumId = null, HistoryEventType? eventType = null, bool includeArtist = false, bool includeAlbum = false, bool includeTrack = false) { - var queryArtistId = Request.Query.ArtistId; - var queryAlbumId = Request.Query.AlbumId; - var queryEventType = Request.Query.EventType; - - if (!queryArtistId.HasValue) - { - throw new BadRequestException("artistId is missing"); - } - - int artistId = Convert.ToInt32(queryArtistId.Value); - HistoryEventType? eventType = null; - var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); - var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum"); - var includeTrack = Request.GetBooleanQueryParameter("includeTrack"); - - if (queryEventType.HasValue) + if (albumId.HasValue) { - eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value); - } - - if (queryAlbumId.HasValue) - { - int albumId = Convert.ToInt32(queryAlbumId.Value); - - return _historyService.GetByAlbum(albumId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList(); + return _historyService.GetByAlbum(albumId.Value, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList(); } return _historyService.GetByArtist(artistId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList(); } - private object MarkAsFailed() + [HttpPost("failed")] + public object MarkAsFailed([FromBody] int id) { - var id = (int)Request.Form.Id; _failedDownloadService.MarkAsFailed(id); return new object(); } diff --git a/src/Lidarr.Api.V1/ImportLists/ImportListModule.cs b/src/Lidarr.Api.V1/ImportLists/ImportListController.cs similarity index 84% rename from src/Lidarr.Api.V1/ImportLists/ImportListModule.cs rename to src/Lidarr.Api.V1/ImportLists/ImportListController.cs index 8f3b1edb1..89735970a 100644 --- a/src/Lidarr.Api.V1/ImportLists/ImportListModule.cs +++ b/src/Lidarr.Api.V1/ImportLists/ImportListController.cs @@ -1,14 +1,16 @@ +using Lidarr.Http; using NzbDrone.Core.ImportLists; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; namespace Lidarr.Api.V1.ImportLists { - public class ImportListModule : ProviderModuleBase + [V1ApiController] + public class ImportListController : ProviderControllerBase { public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper(); - public ImportListModule(ImportListFactory importListFactory, + public ImportListController(ImportListFactory importListFactory, QualityProfileExistsValidator qualityProfileExistsValidator, MetadataProfileExistsValidator metadataProfileExistsValidator) : base(importListFactory, "importlist", ResourceMapper) diff --git a/src/Lidarr.Api.V1/ImportLists/ImportListExclusionModule.cs b/src/Lidarr.Api.V1/ImportLists/ImportListExclusionController.cs similarity index 56% rename from src/Lidarr.Api.V1/ImportLists/ImportListExclusionModule.cs rename to src/Lidarr.Api.V1/ImportLists/ImportListExclusionController.cs index 51ea2307d..b5b30f1cd 100644 --- a/src/Lidarr.Api.V1/ImportLists/ImportListExclusionModule.cs +++ b/src/Lidarr.Api.V1/ImportLists/ImportListExclusionController.cs @@ -1,54 +1,57 @@ using System.Collections.Generic; using FluentValidation; using Lidarr.Http; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.ImportLists.Exclusions; using NzbDrone.Core.Validation; namespace Lidarr.Api.V1.ImportLists { - public class ImportListExclusionModule : LidarrRestModule + [V1ApiController] + public class ImportListExclusionController : RestController { private readonly IImportListExclusionService _importListExclusionService; - public ImportListExclusionModule(IImportListExclusionService importListExclusionService, + public ImportListExclusionController(IImportListExclusionService importListExclusionService, ImportListExclusionExistsValidator importListExclusionExistsValidator, GuidValidator guidValidator) { _importListExclusionService = importListExclusionService; - GetResourceById = GetImportListExclusion; - GetResourceAll = GetImportListExclusions; - CreateResource = AddImportListExclusion; - UpdateResource = UpdateImportListExclusion; - DeleteResource = DeleteImportListExclusionResource; - SharedValidator.RuleFor(c => c.ForeignId).NotEmpty().SetValidator(guidValidator).SetValidator(importListExclusionExistsValidator); SharedValidator.RuleFor(c => c.ArtistName).NotEmpty(); } - private ImportListExclusionResource GetImportListExclusion(int id) + public override ImportListExclusionResource GetResourceById(int id) { return _importListExclusionService.Get(id).ToResource(); } - private List GetImportListExclusions() + [HttpGet] + public List GetImportListExclusions() { return _importListExclusionService.All().ToResource(); } - private int AddImportListExclusion(ImportListExclusionResource resource) + [RestPostById] + public ActionResult AddImportListExclusion(ImportListExclusionResource resource) { var customFilter = _importListExclusionService.Add(resource.ToModel()); - return customFilter.Id; + return Created(customFilter.Id); } - private void UpdateImportListExclusion(ImportListExclusionResource resource) + [RestPutById] + public ActionResult UpdateImportListExclusion(ImportListExclusionResource resource) { _importListExclusionService.Update(resource.ToModel()); + return Accepted(resource.Id); } - private void DeleteImportListExclusionResource(int id) + [RestDeleteById] + public void DeleteImportListExclusionResource(int id) { _importListExclusionService.Delete(id); } diff --git a/src/Lidarr.Api.V1/Indexers/IndexerModule.cs b/src/Lidarr.Api.V1/Indexers/IndexerController.cs similarity index 67% rename from src/Lidarr.Api.V1/Indexers/IndexerModule.cs rename to src/Lidarr.Api.V1/Indexers/IndexerController.cs index 5fb1168e9..c54292e37 100644 --- a/src/Lidarr.Api.V1/Indexers/IndexerModule.cs +++ b/src/Lidarr.Api.V1/Indexers/IndexerController.cs @@ -1,12 +1,14 @@ -using NzbDrone.Core.Indexers; +using Lidarr.Http; +using NzbDrone.Core.Indexers; namespace Lidarr.Api.V1.Indexers { - public class IndexerModule : ProviderModuleBase + [V1ApiController] + public class IndexerController : ProviderControllerBase { public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); - public IndexerModule(IndexerFactory indexerFactory) + public IndexerController(IndexerFactory indexerFactory) : base(indexerFactory, "indexer", ResourceMapper) { } diff --git a/src/Lidarr.Api.V1/Indexers/ReleaseModule.cs b/src/Lidarr.Api.V1/Indexers/ReleaseController.cs similarity index 91% rename from src/Lidarr.Api.V1/Indexers/ReleaseModule.cs rename to src/Lidarr.Api.V1/Indexers/ReleaseController.cs index a2d28bd7d..49e81e867 100644 --- a/src/Lidarr.Api.V1/Indexers/ReleaseModule.cs +++ b/src/Lidarr.Api.V1/Indexers/ReleaseController.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; using FluentValidation; -using Nancy; +using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; @@ -18,7 +19,8 @@ using HttpStatusCode = System.Net.HttpStatusCode; namespace Lidarr.Api.V1.Indexers { - public class ReleaseModule : ReleaseModuleBase + [V1ApiController] + public class ReleaseController : ReleaseControllerBase { private readonly IAlbumService _albumService; private readonly IArtistService _artistService; @@ -32,7 +34,7 @@ namespace Lidarr.Api.V1.Indexers private readonly ICached _remoteAlbumCache; - public ReleaseModule(IAlbumService albumService, + public ReleaseController(IAlbumService albumService, IArtistService artistService, IFetchAndParseRss rssFetcherAndParser, ISearchForNzb nzbSearchService, @@ -53,17 +55,17 @@ namespace Lidarr.Api.V1.Indexers _downloadService = downloadService; _logger = logger; - GetResourceAll = GetReleases; - Post("/", x => DownloadRelease(ReadResourceFromRequest())); - PostValidator.RuleFor(s => s.IndexerId).ValidId(); PostValidator.RuleFor(s => s.Guid).NotEmpty(); _remoteAlbumCache = cacheManager.GetCache(GetType(), "remoteAlbums"); } - private object DownloadRelease(ReleaseResource release) + [HttpPost] + public ActionResult Create(ReleaseResource release) { + ValidateResource(release); + var remoteAlbum = _remoteAlbumCache.Find(GetCacheKey(release)); if (remoteAlbum == null) @@ -129,19 +131,20 @@ namespace Lidarr.Api.V1.Indexers throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); } - return release; + return Ok(release); } - private List GetReleases() + [HttpGet] + public List GetReleases(int? albumId, int? artistId) { - if (Request.Query.albumId.HasValue) + if (albumId.HasValue) { - return GetAlbumReleases(Request.Query.albumId); + return GetAlbumReleases(int.Parse(Request.Query["albumId"])); } - if (Request.Query.artistId.HasValue) + if (artistId.HasValue) { - return GetArtistReleases(Request.Query.artistId); + return GetArtistReleases(int.Parse(Request.Query["artistId"])); } return GetRss(); diff --git a/src/Lidarr.Api.V1/Indexers/ReleaseModuleBase.cs b/src/Lidarr.Api.V1/Indexers/ReleaseControllerBase.cs similarity index 82% rename from src/Lidarr.Api.V1/Indexers/ReleaseModuleBase.cs rename to src/Lidarr.Api.V1/Indexers/ReleaseControllerBase.cs index 7f685664c..9570f3ec8 100644 --- a/src/Lidarr.Api.V1/Indexers/ReleaseModuleBase.cs +++ b/src/Lidarr.Api.V1/Indexers/ReleaseControllerBase.cs @@ -1,11 +1,17 @@ +using System; using System.Collections.Generic; -using Lidarr.Http; +using Lidarr.Http.REST; using NzbDrone.Core.DecisionEngine; namespace Lidarr.Api.V1.Indexers { - public abstract class ReleaseModuleBase : LidarrRestModule + public abstract class ReleaseControllerBase : RestController { + public override ReleaseResource GetResourceById(int id) + { + throw new NotImplementedException(); + } + protected virtual List MapDecisions(IEnumerable decisions) { var result = new List(); diff --git a/src/Lidarr.Api.V1/Indexers/ReleasePushModule.cs b/src/Lidarr.Api.V1/Indexers/ReleasePushController.cs similarity index 90% rename from src/Lidarr.Api.V1/Indexers/ReleasePushModule.cs rename to src/Lidarr.Api.V1/Indexers/ReleasePushController.cs index 2b756c1e8..9e0cd1b30 100644 --- a/src/Lidarr.Api.V1/Indexers/ReleasePushModule.cs +++ b/src/Lidarr.Api.V1/Indexers/ReleasePushController.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; +using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; @@ -12,14 +14,15 @@ using NzbDrone.Core.Parser.Model; namespace Lidarr.Api.V1.Indexers { - internal class ReleasePushModule : ReleaseModuleBase + [V1ApiController("release/push")] + public class ReleasePushController : ReleaseControllerBase { private readonly IMakeDownloadDecision _downloadDecisionMaker; private readonly IProcessDownloadDecisions _downloadDecisionProcessor; private readonly IIndexerFactory _indexerFactory; private readonly Logger _logger; - public ReleasePushModule(IMakeDownloadDecision downloadDecisionMaker, + public ReleasePushController(IMakeDownloadDecision downloadDecisionMaker, IProcessDownloadDecisions downloadDecisionProcessor, IIndexerFactory indexerFactory, Logger logger) @@ -29,18 +32,19 @@ namespace Lidarr.Api.V1.Indexers _indexerFactory = indexerFactory; _logger = logger; - Post("/push", x => ProcessRelease(ReadResourceFromRequest())); - PostValidator.RuleFor(s => s.Title).NotEmpty(); PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty(); PostValidator.RuleFor(s => s.Protocol).NotEmpty(); PostValidator.RuleFor(s => s.PublishDate).NotEmpty(); } - private object ProcessRelease(ReleaseResource release) + [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; diff --git a/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj b/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj index 9b598f5a4..fec628393 100644 --- a/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj +++ b/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj @@ -9,11 +9,9 @@ + - - - diff --git a/src/Lidarr.Api.V1/LidarrV1FeedModule.cs b/src/Lidarr.Api.V1/LidarrV1FeedModule.cs deleted file mode 100644 index fe83432b4..000000000 --- a/src/Lidarr.Api.V1/LidarrV1FeedModule.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Lidarr.Http; - -namespace Lidarr.Api.V1 -{ - public abstract class LidarrV1FeedModule : LidarrModule - { - protected LidarrV1FeedModule(string resource) - : base("/feed/v1/" + resource.Trim('/')) - { - } - } -} diff --git a/src/Lidarr.Api.V1/LidarrV1Module.cs b/src/Lidarr.Api.V1/LidarrV1Module.cs deleted file mode 100644 index 3fbbf54b4..000000000 --- a/src/Lidarr.Api.V1/LidarrV1Module.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Lidarr.Http; - -namespace Lidarr.Api.V1 -{ - public abstract class LidarrV1Module : LidarrModule - { - protected LidarrV1Module(string resource) - : base("/api/v1/" + resource.Trim('/')) - { - } - } -} diff --git a/src/Lidarr.Api.V1/Logs/LogModule.cs b/src/Lidarr.Api.V1/Logs/LogController.cs similarity index 82% rename from src/Lidarr.Api.V1/Logs/LogModule.cs rename to src/Lidarr.Api.V1/Logs/LogController.cs index ba9c61312..b9808e97f 100644 --- a/src/Lidarr.Api.V1/Logs/LogModule.cs +++ b/src/Lidarr.Api.V1/Logs/LogController.cs @@ -1,21 +1,25 @@ using System.Linq; using Lidarr.Http; +using Lidarr.Http.Extensions; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Instrumentation; namespace Lidarr.Api.V1.Logs { - public class LogModule : LidarrRestModule + [V1ApiController] + public class LogController : Controller { private readonly ILogService _logService; - public LogModule(ILogService logService) + public LogController(ILogService logService) { _logService = logService; - GetResourcePaged = GetLogs; } - private PagingResource GetLogs(PagingResource pagingResource) + [HttpGet] + public PagingResource GetLogs() { + var pagingResource = Request.ReadPagingResourceFromRequest(); var pageSpec = pagingResource.MapToPagingSpec(); if (pageSpec.SortKey == "time") @@ -50,7 +54,7 @@ namespace Lidarr.Api.V1.Logs } } - var response = ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource); + var response = pageSpec.ApplyToPage(_logService.Paged, LogResourceMapper.ToResource); if (pageSpec.SortKey == "id") { diff --git a/src/Lidarr.Api.V1/Logs/LogFileModule.cs b/src/Lidarr.Api.V1/Logs/LogFileController.cs similarity index 83% rename from src/Lidarr.Api.V1/Logs/LogFileModule.cs rename to src/Lidarr.Api.V1/Logs/LogFileController.cs index a217a58d5..10e72debe 100644 --- a/src/Lidarr.Api.V1/Logs/LogFileModule.cs +++ b/src/Lidarr.Api.V1/Logs/LogFileController.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; +using Lidarr.Http; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -7,12 +8,13 @@ using NzbDrone.Core.Configuration; namespace Lidarr.Api.V1.Logs { - public class LogFileModule : LogFileModuleBase + [V1ApiController("log/file")] + public class LogFileController : LogFileControllerBase { private readonly IAppFolderInfo _appFolderInfo; private readonly IDiskProvider _diskProvider; - public LogFileModule(IAppFolderInfo appFolderInfo, + public LogFileController(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider) : base(diskProvider, configFileProvider, "") diff --git a/src/Lidarr.Api.V1/Logs/LogFileModuleBase.cs b/src/Lidarr.Api.V1/Logs/LogFileControllerBase.cs similarity index 70% rename from src/Lidarr.Api.V1/Logs/LogFileModuleBase.cs rename to src/Lidarr.Api.V1/Logs/LogFileControllerBase.cs index c308cf2b3..6379b89df 100644 --- a/src/Lidarr.Api.V1/Logs/LogFileModuleBase.cs +++ b/src/Lidarr.Api.V1/Logs/LogFileControllerBase.cs @@ -1,35 +1,32 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Lidarr.Http; -using Nancy; -using Nancy.Responses; +using Microsoft.AspNetCore.Mvc; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; namespace Lidarr.Api.V1.Logs { - public abstract class LogFileModuleBase : LidarrRestModule + 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 LogFileModuleBase(IDiskProvider diskProvider, + public LogFileControllerBase(IDiskProvider diskProvider, IConfigFileProvider configFileProvider, - string route) - : base("log/file" + route) + string resource) { _diskProvider = diskProvider; _configFileProvider = configFileProvider; - GetResourceAll = GetLogFilesResponse; - - Get(LOGFILE_ROUTE, options => GetLogFileResponse(options.filename)); + _resource = resource; } - private List GetLogFilesResponse() + [HttpGet] + public List GetLogFilesResponse() { var result = new List(); @@ -45,7 +42,7 @@ namespace Lidarr.Api.V1.Logs Id = i + 1, Filename = filename, LastWriteTime = _diskProvider.FileGetLastWrite(file), - ContentsUrl = string.Format("{0}/api/v1/{1}/{2}", _configFileProvider.UrlBase, Resource, filename), + ContentsUrl = string.Format("{0}/api/v1/{1}/{2}", _configFileProvider.UrlBase, _resource, filename), DownloadUrl = string.Format("{0}/{1}/{2}", _configFileProvider.UrlBase, DownloadUrlRoot, filename) }); } @@ -53,7 +50,8 @@ namespace Lidarr.Api.V1.Logs return result.OrderByDescending(l => l.LastWriteTime).ToList(); } - private object GetLogFileResponse(string filename) + [HttpGet(@"{filename:regex([[-.a-zA-Z0-9]]+?\.txt)}")] + public IActionResult GetLogFileResponse(string filename) { LogManager.Flush(); @@ -61,12 +59,10 @@ namespace Lidarr.Api.V1.Logs if (!_diskProvider.FileExists(filePath)) { - return new NotFoundResponse(); + return NotFound(); } - var data = _diskProvider.ReadAllText(filePath); - - return new TextResponse(data); + return PhysicalFile(filePath, "text/plain"); } protected abstract IEnumerable GetLogFiles(); diff --git a/src/Lidarr.Api.V1/Logs/UpdateLogFileModule.cs b/src/Lidarr.Api.V1/Logs/UpdateLogFileController.cs similarity index 83% rename from src/Lidarr.Api.V1/Logs/UpdateLogFileModule.cs rename to src/Lidarr.Api.V1/Logs/UpdateLogFileController.cs index 83680b94c..b808e3c05 100644 --- a/src/Lidarr.Api.V1/Logs/UpdateLogFileModule.cs +++ b/src/Lidarr.Api.V1/Logs/UpdateLogFileController.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Lidarr.Http; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -9,15 +10,16 @@ using NzbDrone.Core.Configuration; namespace Lidarr.Api.V1.Logs { - public class UpdateLogFileModule : LogFileModuleBase + [V1ApiController("log/file/update")] + public class UpdateLogFileController : LogFileControllerBase { private readonly IAppFolderInfo _appFolderInfo; private readonly IDiskProvider _diskProvider; - public UpdateLogFileModule(IAppFolderInfo appFolderInfo, + public UpdateLogFileController(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider) - : base(diskProvider, configFileProvider, "/update") + : base(diskProvider, configFileProvider, "update") { _appFolderInfo = appFolderInfo; _diskProvider = diskProvider; diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs similarity index 71% rename from src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs rename to src/Lidarr.Api.V1/ManualImport/ManualImportController.cs index 73199ec31..eab2f1b83 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs @@ -1,9 +1,7 @@ -using System; using System.Collections.Generic; using System.Linq; using Lidarr.Http; -using Lidarr.Http.Extensions; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NLog; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport.Manual; @@ -12,7 +10,8 @@ using NzbDrone.Core.Qualities; namespace Lidarr.Api.V1.ManualImport { - public class ManualImportModule : LidarrRestModule + [V1ApiController] + public class ManualImportController : Controller { private readonly IArtistService _artistService; private readonly IAlbumService _albumService; @@ -20,7 +19,7 @@ namespace Lidarr.Api.V1.ManualImport private readonly IManualImportService _manualImportService; private readonly Logger _logger; - public ManualImportModule(IManualImportService manualImportService, + public ManualImportController(IManualImportService manualImportService, IArtistService artistService, IAlbumService albumService, IReleaseService releaseService, @@ -31,31 +30,25 @@ namespace Lidarr.Api.V1.ManualImport _releaseService = releaseService; _manualImportService = manualImportService; _logger = logger; + } - GetResourceAll = GetMediaFiles; - - Put("/", options => - { - var resource = Request.Body.FromJson>(); - return ResponseWithCode(UpdateImportItems(resource), HttpStatusCode.Accepted); - }); + [HttpPut] + public IActionResult UpdateItems(List resource) + { + return Accepted(UpdateImportItems(resource)); } - private List GetMediaFiles() + [HttpGet] + public List GetMediaFiles(string folder, string downloadId, int? artistId, bool filterExistingFiles = true, bool replaceExistingFiles = true) { - var folder = (string)Request.Query.folder; - var downloadId = (string)Request.Query.downloadId; NzbDrone.Core.Music.Artist artist = null; - var artistIdQuery = Request.GetNullableIntegerQueryParameter("artistId", null); - - if (artistIdQuery.HasValue && artistIdQuery.Value > 0) + if (artistId > 0) { - artist = _artistService.GetArtist(Convert.ToInt32(artistIdQuery.Value)); + artist = _artistService.GetArtist(artistId.Value); } - var filter = Request.GetBooleanQueryParameter("filterExistingFiles", true) ? FilterFilesType.Matched : FilterFilesType.None; - var replaceExistingFiles = Request.GetBooleanQueryParameter("replaceExistingFiles", true); + var filter = filterExistingFiles ? FilterFilesType.Matched : FilterFilesType.None; return _manualImportService.GetMediaFiles(folder, downloadId, artist, filter, replaceExistingFiles).ToResource().Select(AddQualityWeight).ToList(); } diff --git a/src/Lidarr.Api.V1/MediaCovers/MediaCoverModule.cs b/src/Lidarr.Api.V1/MediaCovers/MediaCoverController.cs similarity index 62% rename from src/Lidarr.Api.V1/MediaCovers/MediaCoverModule.cs rename to src/Lidarr.Api.V1/MediaCovers/MediaCoverController.cs index d383019d4..92e164729 100644 --- a/src/Lidarr.Api.V1/MediaCovers/MediaCoverModule.cs +++ b/src/Lidarr.Api.V1/MediaCovers/MediaCoverController.cs @@ -1,34 +1,32 @@ using System.IO; using System.Text.RegularExpressions; -using Nancy; -using Nancy.Responses; +using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; namespace Lidarr.Api.V1.MediaCovers { - public class MediaCoverModule : LidarrV1Module + [V1ApiController] + public class MediaCoverController : Controller { - private const string MEDIA_COVER_ARTIST_ROUTE = @"/Artist/(?\d+)/(?(.+)\.(jpg|png|gif))"; - private const string MEDIA_COVER_ALBUM_ROUTE = @"/Album/(?\d+)/(?(.+)\.(jpg|png|gif))"; - private static readonly Regex RegexResizedImage = new Regex(@"-\d+(?=\.(jpg|png|gif)$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private readonly IAppFolderInfo _appFolderInfo; private readonly IDiskProvider _diskProvider; + private readonly IContentTypeProvider _mimeTypeProvider; - public MediaCoverModule(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) - : base("MediaCover") + public MediaCoverController(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) { _appFolderInfo = appFolderInfo; _diskProvider = diskProvider; - - Get(MEDIA_COVER_ARTIST_ROUTE, options => GetArtistMediaCover(options.artistId, options.filename)); - Get(MEDIA_COVER_ALBUM_ROUTE, options => GetAlbumMediaCover(options.artistId, options.filename)); + _mimeTypeProvider = new FileExtensionContentTypeProvider(); } - private object GetArtistMediaCover(int artistId, string filename) + [HttpGet(@"artist/{artistId:int}/{filename:regex((.+)\.(jpg|png|gif))}")] + public IActionResult GetArtistMediaCover(int artistId, string filename) { var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", artistId.ToString(), filename); @@ -39,16 +37,17 @@ namespace Lidarr.Api.V1.MediaCovers var basefilePath = RegexResizedImage.Replace(filePath, ""); if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath)) { - return new NotFoundResponse(); + return NotFound(); } filePath = basefilePath; } - return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath)); + return PhysicalFile(filePath, GetContentType(filePath)); } - private object GetAlbumMediaCover(int albumId, string filename) + [HttpGet(@"album/{albumId:int}/{filename:regex((.+)\.(jpg|png|gif))}")] + public IActionResult GetAlbumMediaCover(int albumId, string filename) { var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", "Albums", albumId.ToString(), filename); @@ -59,13 +58,23 @@ namespace Lidarr.Api.V1.MediaCovers var basefilePath = RegexResizedImage.Replace(filePath, ""); if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath)) { - return new NotFoundResponse(); + return NotFound(); } filePath = basefilePath; } - return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath)); + 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/Lidarr.Api.V1/Metadata/MetadataModule.cs b/src/Lidarr.Api.V1/Metadata/MetadataController.cs similarity index 65% rename from src/Lidarr.Api.V1/Metadata/MetadataModule.cs rename to src/Lidarr.Api.V1/Metadata/MetadataController.cs index f3f452fcc..0e4438710 100644 --- a/src/Lidarr.Api.V1/Metadata/MetadataModule.cs +++ b/src/Lidarr.Api.V1/Metadata/MetadataController.cs @@ -1,12 +1,14 @@ -using NzbDrone.Core.Extras.Metadata; +using Lidarr.Http; +using NzbDrone.Core.Extras.Metadata; namespace Lidarr.Api.V1.Metadata { - public class MetadataModule : ProviderModuleBase + [V1ApiController] + public class MetadataController : ProviderControllerBase { public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper(); - public MetadataModule(IMetadataFactory metadataFactory) + public MetadataController(IMetadataFactory metadataFactory) : base(metadataFactory, "metadata", ResourceMapper) { } diff --git a/src/Lidarr.Api.V1/Notifications/NotificationModule.cs b/src/Lidarr.Api.V1/Notifications/NotificationController.cs similarity index 64% rename from src/Lidarr.Api.V1/Notifications/NotificationModule.cs rename to src/Lidarr.Api.V1/Notifications/NotificationController.cs index 49825e513..c1883d8a4 100644 --- a/src/Lidarr.Api.V1/Notifications/NotificationModule.cs +++ b/src/Lidarr.Api.V1/Notifications/NotificationController.cs @@ -1,12 +1,14 @@ -using NzbDrone.Core.Notifications; +using Lidarr.Http; +using NzbDrone.Core.Notifications; namespace Lidarr.Api.V1.Notifications { - public class NotificationModule : ProviderModuleBase + [V1ApiController] + public class NotificationController : ProviderControllerBase { public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper(); - public NotificationModule(NotificationFactory notificationFactory) + public NotificationController(NotificationFactory notificationFactory) : base(notificationFactory, "notification", ResourceMapper) { } diff --git a/src/Lidarr.Api.V1/Parse/ParseModule.cs b/src/Lidarr.Api.V1/Parse/ParseController.cs similarity index 80% rename from src/Lidarr.Api.V1/Parse/ParseModule.cs rename to src/Lidarr.Api.V1/Parse/ParseController.cs index d1cf0d49a..38e2b89f6 100644 --- a/src/Lidarr.Api.V1/Parse/ParseModule.cs +++ b/src/Lidarr.Api.V1/Parse/ParseController.cs @@ -1,24 +1,24 @@ using Lidarr.Api.V1.Albums; using Lidarr.Api.V1.Artist; using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Parser; namespace Lidarr.Api.V1.Parse { - public class ParseModule : LidarrRestModule + [V1ApiController] + public class ParseController : Controller { private readonly IParsingService _parsingService; - public ParseModule(IParsingService parsingService) + public ParseController(IParsingService parsingService) { _parsingService = parsingService; - - GetResourceSingle = Parse; } - private ParseResource Parse() + [HttpGet] + public ParseResource Parse(string title) { - var title = Request.Query.Title.Value as string; var parsedAlbumInfo = Parser.ParseAlbumTitle(title); if (parsedAlbumInfo == null) diff --git a/src/Lidarr.Api.V1/Profiles/Delay/DelayProfileModule.cs b/src/Lidarr.Api.V1/Profiles/Delay/DelayProfileController.cs similarity index 65% rename from src/Lidarr.Api.V1/Profiles/Delay/DelayProfileModule.cs rename to src/Lidarr.Api.V1/Profiles/Delay/DelayProfileController.cs index 80035815c..414d26fae 100644 --- a/src/Lidarr.Api.V1/Profiles/Delay/DelayProfileModule.cs +++ b/src/Lidarr.Api.V1/Profiles/Delay/DelayProfileController.cs @@ -1,29 +1,23 @@ -using System; using System.Collections.Generic; using FluentValidation; using Lidarr.Http; using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; using Lidarr.Http.Validation; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Profiles.Delay; namespace Lidarr.Api.V1.Profiles.Delay { - public class DelayProfileModule : LidarrRestModule + [V1ApiController] + public class DelayProfileController : RestController { private readonly IDelayProfileService _delayProfileService; - public DelayProfileModule(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator) + public DelayProfileController(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator) { _delayProfileService = delayProfileService; - GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - CreateResource = Create; - DeleteResource = DeleteProfile; - Put(@"/reorder/(?[\d]{1,10})", options => Reorder(options.Id)); - 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); @@ -39,15 +33,17 @@ namespace Lidarr.Api.V1.Profiles.Delay }); } - private int Create(DelayProfileResource resource) + [RestPostById] + public ActionResult Create(DelayProfileResource resource) { var model = resource.ToModel(); model = _delayProfileService.Add(model); - return model.Id; + return Created(model.Id); } - private void DeleteProfile(int id) + [RestDeleteById] + public void DeleteProfile(int id) { if (id == 1) { @@ -57,29 +53,30 @@ namespace Lidarr.Api.V1.Profiles.Delay _delayProfileService.Delete(id); } - private void Update(DelayProfileResource resource) + [RestPutById] + public ActionResult Update(DelayProfileResource resource) { var model = resource.ToModel(); _delayProfileService.Update(model); + return Accepted(model.Id); } - private DelayProfileResource GetById(int id) + public override DelayProfileResource GetResourceById(int id) { return _delayProfileService.Get(id).ToResource(); } - private List GetAll() + [HttpGet] + public List GetAll() { return _delayProfileService.All().ToResource(); } - private object Reorder(int id) + [HttpPut("reorder/{id:int}")] + public object Reorder(int id, [FromQuery] int? afterId = null) { ValidateId(id); - var afterIdQuery = Request.Query.After; - int? afterId = afterIdQuery.HasValue ? Convert.ToInt32(afterIdQuery.Value) : null; - return _delayProfileService.Reorder(id, afterId).ToResource(); } } diff --git a/src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileModule.cs b/src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileController.cs similarity index 59% rename from src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileModule.cs rename to src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileController.cs index 1006a4185..1813003bd 100644 --- a/src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileModule.cs +++ b/src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileController.cs @@ -1,54 +1,59 @@ using System.Collections.Generic; using FluentValidation; using Lidarr.Http; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Profiles.Metadata; namespace Lidarr.Api.V1.Profiles.Metadata { - public class MetadataProfileModule : LidarrRestModule + [V1ApiController] + public class MetadataProfileController : RestController { private readonly IMetadataProfileService _profileService; - public MetadataProfileModule(IMetadataProfileService profileService) + public MetadataProfileController(IMetadataProfileService profileService) { _profileService = profileService; + SharedValidator.RuleFor(c => c.Name).NotEqual("None").WithMessage("'None' is a reserved profile name").NotEmpty(); SharedValidator.RuleFor(c => c.PrimaryAlbumTypes).MustHaveAllowedPrimaryType(); SharedValidator.RuleFor(c => c.SecondaryAlbumTypes).MustHaveAllowedSecondaryType(); SharedValidator.RuleFor(c => c.ReleaseStatuses).MustHaveAllowedReleaseStatus(); - - GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - CreateResource = Create; - DeleteResource = DeleteProfile; } - private int Create(MetadataProfileResource resource) + [RestPostById] + public ActionResult Create(MetadataProfileResource resource) { var model = resource.ToModel(); model = _profileService.Add(model); - return model.Id; + return Created(model.Id); } - private void DeleteProfile(int id) + [RestDeleteById] + public void DeleteProfile(int id) { _profileService.Delete(id); } - private void Update(MetadataProfileResource resource) + [RestPutById] + public ActionResult Update(MetadataProfileResource resource) { var model = resource.ToModel(); _profileService.Update(model); + + return Accepted(model.Id); } - private MetadataProfileResource GetById(int id) + public override MetadataProfileResource GetResourceById(int id) { return _profileService.Get(id).ToResource(); } - private List GetAll() + [HttpGet] + public List GetAll() { var profiles = _profileService.All().ToResource(); diff --git a/src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileSchemaModule.cs b/src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileSchemaController.cs similarity index 82% rename from src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileSchemaModule.cs rename to src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileSchemaController.cs index 6a4619e03..361fb2ee4 100644 --- a/src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileSchemaModule.cs +++ b/src/Lidarr.Api.V1/Profiles/Metadata/MetadataProfileSchemaController.cs @@ -1,18 +1,15 @@ using System.Linq; using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Profiles.Metadata; namespace Lidarr.Api.V1.Profiles.Metadata { - public class MetadataProfileSchemaModule : LidarrRestModule + [V1ApiController("metadataprofile/schema")] + public class MetadataProfileSchemaController : Controller { - public MetadataProfileSchemaModule() - : base("/metadataprofile/schema") - { - GetResourceSingle = GetAll; - } - - private MetadataProfileResource GetAll() + [HttpGet] + public MetadataProfileResource GetAll() { var orderedPrimTypes = NzbDrone.Core.Music.PrimaryAlbumType.All .OrderByDescending(l => l.Id) diff --git a/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileModule.cs b/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileController.cs similarity index 54% rename from src/Lidarr.Api.V1/Profiles/Quality/QualityProfileModule.cs rename to src/Lidarr.Api.V1/Profiles/Quality/QualityProfileController.cs index 9146d9000..fcb7f8ceb 100644 --- a/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileModule.cs +++ b/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileController.cs @@ -1,53 +1,57 @@ using System.Collections.Generic; using FluentValidation; using Lidarr.Http; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Profiles.Qualities; namespace Lidarr.Api.V1.Profiles.Quality { - public class ProfileModule : LidarrRestModule + [V1ApiController] + public class QualityProfileController : RestController { private readonly IProfileService _profileService; - public ProfileModule(IProfileService profileService) + public QualityProfileController(IProfileService profileService) { _profileService = profileService; SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff(); SharedValidator.RuleFor(c => c.Items).ValidItems(); - - GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - CreateResource = Create; - DeleteResource = DeleteProfile; } - private int Create(QualityProfileResource resource) + [RestPostById] + public ActionResult Create(QualityProfileResource resource) { var model = resource.ToModel(); model = _profileService.Add(model); - return model.Id; + return Created(model.Id); } - private void DeleteProfile(int id) + [RestDeleteById] + public void DeleteProfile(int id) { _profileService.Delete(id); } - private void Update(QualityProfileResource resource) + [RestPutById] + public ActionResult Update(QualityProfileResource resource) { var model = resource.ToModel(); _profileService.Update(model); + + return Accepted(model.Id); } - private QualityProfileResource GetById(int id) + public override QualityProfileResource GetResourceById(int id) { return _profileService.Get(id).ToResource(); } - private List GetAll() + [HttpGet] + public List GetAll() { return _profileService.All().ToResource(); } diff --git a/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileSchemaModule.cs b/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileSchemaController.cs similarity index 57% rename from src/Lidarr.Api.V1/Profiles/Quality/QualityProfileSchemaModule.cs rename to src/Lidarr.Api.V1/Profiles/Quality/QualityProfileSchemaController.cs index d3a1dd3c9..188255bd2 100644 --- a/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileSchemaModule.cs +++ b/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileSchemaController.cs @@ -1,20 +1,21 @@ using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Profiles.Qualities; namespace Lidarr.Api.V1.Profiles.Quality { - public class QualityProfileSchemaModule : LidarrRestModule + [V1ApiController("qualityprofile/schema")] + public class QualityProfileSchemaController : Controller { private readonly IProfileService _profileService; - public QualityProfileSchemaModule(IProfileService profileService) - : base("/qualityprofile/schema") + public QualityProfileSchemaController(IProfileService profileService) { _profileService = profileService; - GetResourceSingle = GetSchema; } - private QualityProfileResource GetSchema() + [HttpGet] + public QualityProfileResource GetSchema() { QualityProfile qualityProfile = _profileService.GetDefaultProfile(string.Empty); diff --git a/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileModule.cs b/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileController.cs similarity index 66% rename from src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileModule.cs rename to src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileController.cs index 1f7ca6daf..5cc4810d3 100644 --- a/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileModule.cs +++ b/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileController.cs @@ -2,28 +2,26 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; using Lidarr.Http; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; using NzbDrone.Core.Profiles.Releases; namespace Lidarr.Api.V1.Profiles.Release { - public class ReleaseProfileModule : LidarrRestModule + [V1ApiController] + public class ReleaseProfileController : RestController { private readonly IReleaseProfileService _releaseProfileService; private readonly IIndexerFactory _indexerFactory; - public ReleaseProfileModule(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory) + public ReleaseProfileController(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory) { _releaseProfileService = releaseProfileService; _indexerFactory = indexerFactory; - GetResourceById = GetById; - GetResourceAll = GetAll; - CreateResource = Create; - UpdateResource = Update; - DeleteResource = DeleteById; - SharedValidator.RuleFor(r => r).Custom((restriction, context) => { if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace() && restriction.Preferred.Empty()) @@ -43,27 +41,32 @@ namespace Lidarr.Api.V1.Profiles.Release }); } - private ReleaseProfileResource GetById(int id) + public override ReleaseProfileResource GetResourceById(int id) { return _releaseProfileService.Get(id).ToResource(); } - private List GetAll() + [HttpGet] + public List GetAll() { return _releaseProfileService.All().ToResource(); } - private int Create(ReleaseProfileResource resource) + [RestPostById] + public ActionResult Create(ReleaseProfileResource resource) { - return _releaseProfileService.Add(resource.ToModel()).Id; + return Created(_releaseProfileService.Add(resource.ToModel()).Id); } - private void Update(ReleaseProfileResource resource) + [RestPutById] + public ActionResult Update(ReleaseProfileResource resource) { _releaseProfileService.Update(resource.ToModel()); + return Accepted(resource.Id); } - private void DeleteById(int id) + [RestDeleteById] + public void DeleteById(int id) { _releaseProfileService.Delete(id); } diff --git a/src/Lidarr.Api.V1/ProviderModuleBase.cs b/src/Lidarr.Api.V1/ProviderControllerBase.cs similarity index 75% rename from src/Lidarr.Api.V1/ProviderModuleBase.cs rename to src/Lidarr.Api.V1/ProviderControllerBase.cs index 791392287..4b77c1ce9 100644 --- a/src/Lidarr.Api.V1/ProviderModuleBase.cs +++ b/src/Lidarr.Api.V1/ProviderControllerBase.cs @@ -2,15 +2,16 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; -using Lidarr.Http; -using Nancy; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Serializer; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace Lidarr.Api.V1 { - public abstract class ProviderModuleBase : LidarrRestModule + public abstract class ProviderControllerBase : RestController where TProviderDefinition : ProviderDefinition, new() where TProvider : IProvider where TProviderResource : ProviderResource, new() @@ -18,23 +19,11 @@ namespace Lidarr.Api.V1 private readonly IProviderFactory _providerFactory; private readonly ProviderResourceMapper _resourceMapper; - protected ProviderModuleBase(IProviderFactory providerFactory, string resource, ProviderResourceMapper resourceMapper) - : base(resource) + protected ProviderControllerBase(IProviderFactory providerFactory, string resource, ProviderResourceMapper resourceMapper) { _providerFactory = providerFactory; _resourceMapper = resourceMapper; - Get("schema", x => GetTemplates()); - Post("test", x => Test(ReadResourceFromRequest(true))); - Post("testall", x => TestAll()); - Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true, true))); - - GetResourceAll = GetAll; - GetResourceById = GetProviderById; - CreateResource = CreateProvider; - UpdateResource = UpdateProvider; - DeleteResource = DeleteProvider; - 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(); @@ -43,7 +32,7 @@ namespace Lidarr.Api.V1 PostValidator.RuleFor(c => c.Fields).NotNull(); } - private TProviderResource GetProviderById(int id) + public override TProviderResource GetResourceById(int id) { var definition = _providerFactory.Get(id); _providerFactory.SetProviderCharacteristics(definition); @@ -51,7 +40,8 @@ namespace Lidarr.Api.V1 return _resourceMapper.ToResource(definition); } - private List GetAll() + [HttpGet] + public List GetAll() { var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName); @@ -67,7 +57,8 @@ namespace Lidarr.Api.V1 return result.OrderBy(p => p.Name).ToList(); } - private int CreateProvider(TProviderResource providerResource) + [RestPostById] + public ActionResult CreateProvider(TProviderResource providerResource) { var providerDefinition = GetDefinition(providerResource, false); @@ -78,10 +69,11 @@ namespace Lidarr.Api.V1 providerDefinition = _providerFactory.Create(providerDefinition); - return providerDefinition.Id; + return Created(providerDefinition.Id); } - private void UpdateProvider(TProviderResource providerResource) + [RestPutById] + public ActionResult UpdateProvider(TProviderResource providerResource) { var providerDefinition = GetDefinition(providerResource, false); var existingDefinition = _providerFactory.Get(providerDefinition.Id); @@ -93,6 +85,8 @@ namespace Lidarr.Api.V1 } _providerFactory.Update(providerDefinition); + + return Accepted(providerResource.Id); } private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true) @@ -107,12 +101,14 @@ namespace Lidarr.Api.V1 return definition; } - private void DeleteProvider(int id) + [RestDeleteById] + public void DeleteProvider(int id) { _providerFactory.Delete(id); } - private object GetTemplates() + [HttpGet("schema")] + public List GetTemplates() { var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList(); @@ -133,7 +129,9 @@ namespace Lidarr.Api.V1 return result; } - private object Test(TProviderResource providerResource) + [SkipValidation(true, false)] + [HttpPost("test")] + public object Test([FromBody] TProviderResource providerResource) { var providerDefinition = GetDefinition(providerResource, true); @@ -142,7 +140,8 @@ namespace Lidarr.Api.V1 return "{}"; } - private object TestAll() + [HttpPost("testall")] + public IActionResult TestAll() { var providerDefinitions = _providerFactory.All() .Where(c => c.Settings.Validate().IsValid && c.Enable) @@ -160,19 +159,20 @@ namespace Lidarr.Api.V1 }); } - return ResponseWithCode(result, result.Any(c => !c.IsValid) ? HttpStatusCode.BadRequest : HttpStatusCode.OK); + return result.Any(c => !c.IsValid) ? BadRequest(result) : Ok(result); } - private object RequestAction(string action, TProviderResource providerResource) + [SkipValidation] + [HttpPost("action/{name}")] + public IActionResult RequestAction(string name, [FromBody] TProviderResource resource) { - var providerDefinition = GetDefinition(providerResource, true, false); + var providerDefinition = GetDefinition(resource, true, false); + + var query = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString()); - var query = ((IDictionary)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString()); + var data = _providerFactory.RequestAction(providerDefinition, name, query); - var data = _providerFactory.RequestAction(providerDefinition, action, query); - Response resp = data.ToJson(); - resp.ContentType = "application/json"; - return resp; + return Content(data.ToJson(), "application/json"); } protected virtual void Validate(TProviderDefinition definition, bool includeWarnings) diff --git a/src/Lidarr.Api.V1/Qualities/QualityDefinitionController.cs b/src/Lidarr.Api.V1/Qualities/QualityDefinitionController.cs new file mode 100644 index 000000000..cf36f422e --- /dev/null +++ b/src/Lidarr.Api.V1/Qualities/QualityDefinitionController.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using Lidarr.Http; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Qualities; + +namespace Lidarr.Api.V1.Qualities +{ + [V1ApiController] + 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); + } + + public 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/Lidarr.Api.V1/Qualities/QualityDefinitionModule.cs b/src/Lidarr.Api.V1/Qualities/QualityDefinitionModule.cs deleted file mode 100644 index 39b740d0a..000000000 --- a/src/Lidarr.Api.V1/Qualities/QualityDefinitionModule.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Lidarr.Http; -using Lidarr.Http.Extensions; -using Nancy; -using NzbDrone.Core.Qualities; - -namespace Lidarr.Api.V1.Qualities -{ - public class QualityDefinitionModule : LidarrRestModule - { - private readonly IQualityDefinitionService _qualityDefinitionService; - - public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService) - { - _qualityDefinitionService = qualityDefinitionService; - - GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - Put("/update", d => UpdateMany()); - } - - private void Update(QualityDefinitionResource resource) - { - var model = resource.ToModel(); - _qualityDefinitionService.Update(model); - } - - private QualityDefinitionResource GetById(int id) - { - return _qualityDefinitionService.GetById(id).ToResource(); - } - - private List GetAll() - { - return _qualityDefinitionService.All().ToResource(); - } - - private object UpdateMany() - { - //Read from request - var qualityDefinitions = Request.Body.FromJson>() - .ToModel() - .ToList(); - - _qualityDefinitionService.UpdateMany(qualityDefinitions); - - return ResponseWithCode(_qualityDefinitionService.All() - .ToResource(), - HttpStatusCode.Accepted); - } - } -} diff --git a/src/Lidarr.Api.V1/Queue/QueueActionController.cs b/src/Lidarr.Api.V1/Queue/QueueActionController.cs new file mode 100644 index 000000000..64121c64b --- /dev/null +++ b/src/Lidarr.Api.V1/Queue/QueueActionController.cs @@ -0,0 +1,55 @@ +using Lidarr.Http; +using Lidarr.Http.REST; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Pending; + +namespace Lidarr.Api.V1.Queue +{ + [V1ApiController("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.RemoteAlbum); + + return new object(); + } + + [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.RemoteAlbum); + } + + return new object(); + } + } +} diff --git a/src/Lidarr.Api.V1/Queue/QueueActionModule.cs b/src/Lidarr.Api.V1/Queue/QueueActionModule.cs deleted file mode 100644 index 15d071cde..000000000 --- a/src/Lidarr.Api.V1/Queue/QueueActionModule.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Collections.Generic; -using Lidarr.Http; -using Lidarr.Http.Extensions; -using Lidarr.Http.REST; -using Nancy; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.Pending; -using NzbDrone.Core.Download.TrackedDownloads; -using NzbDrone.Core.Queue; - -namespace Lidarr.Api.V1.Queue -{ - public class QueueActionModule : LidarrRestModule - { - private readonly IQueueService _queueService; - private readonly ITrackedDownloadService _trackedDownloadService; - private readonly IFailedDownloadService _failedDownloadService; - private readonly IIgnoredDownloadService _ignoredDownloadService; - private readonly IProvideDownloadClient _downloadClientProvider; - private readonly IPendingReleaseService _pendingReleaseService; - private readonly IDownloadService _downloadService; - - public QueueActionModule(IQueueService queueService, - ITrackedDownloadService trackedDownloadService, - IFailedDownloadService failedDownloadService, - IIgnoredDownloadService ignoredDownloadService, - IProvideDownloadClient downloadClientProvider, - IPendingReleaseService pendingReleaseService, - IDownloadService downloadService) - { - _queueService = queueService; - _trackedDownloadService = trackedDownloadService; - _failedDownloadService = failedDownloadService; - _ignoredDownloadService = ignoredDownloadService; - _downloadClientProvider = downloadClientProvider; - _pendingReleaseService = pendingReleaseService; - _downloadService = downloadService; - - Post(@"/grab/(?[\d]{1,10})", x => Grab((int)x.Id)); - Post("/grab/bulk", x => Grab()); - - Delete(@"/(?[\d]{1,10})", x => Remove((int)x.Id)); - Delete("/bulk", x => Remove()); - } - - private object Grab(int id) - { - var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); - - if (pendingRelease == null) - { - throw new NotFoundException(); - } - - _downloadService.DownloadReport(pendingRelease.RemoteAlbum); - - return new object(); - } - - private object Grab() - { - var resource = Request.Body.FromJson(); - - foreach (var id in resource.Ids) - { - var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); - - if (pendingRelease == null) - { - throw new NotFoundException(); - } - - _downloadService.DownloadReport(pendingRelease.RemoteAlbum); - } - - return new object(); - } - - private object Remove(int id) - { - var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true); - var blacklist = Request.GetBooleanQueryParameter("blacklist"); - var skipReDownload = Request.GetBooleanQueryParameter("skipredownload"); - - var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload); - - if (trackedDownload != null) - { - _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); - } - - return new object(); - } - - private object Remove() - { - var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true); - var blacklist = Request.GetBooleanQueryParameter("blacklist"); - var skipReDownload = Request.GetBooleanQueryParameter("skipredownload"); - - var resource = Request.Body.FromJson(); - var trackedDownloadIds = new List(); - - foreach (var id in resource.Ids) - { - var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload); - - if (trackedDownload != null) - { - trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); - } - } - - _trackedDownloadService.StopTracking(trackedDownloadIds); - - return new object(); - } - - private TrackedDownload Remove(int id, bool removeFromClient, bool blacklist, bool skipReDownload) - { - var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); - - if (pendingRelease != null) - { - _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.DownloadId, true); - } - - if (blacklist) - { - _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipReDownload); - } - - if (!removeFromClient && !blacklist) - { - 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; - } - } -} diff --git a/src/Lidarr.Api.V1/Queue/QueueModule.cs b/src/Lidarr.Api.V1/Queue/QueueController.cs similarity index 56% rename from src/Lidarr.Api.V1/Queue/QueueModule.cs rename to src/Lidarr.Api.V1/Queue/QueueController.cs index c819e8deb..8c444e264 100644 --- a/src/Lidarr.Api.V1/Queue/QueueModule.cs +++ b/src/Lidarr.Api.V1/Queue/QueueController.cs @@ -1,11 +1,17 @@ using System; +using System.Collections.Generic; using System.Linq; using Lidarr.Http; using Lidarr.Http.Extensions; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; 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.Messaging.Events; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; @@ -14,35 +20,82 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Queue { - public class QueueModule : LidarrRestModuleWithSignalR, + [V1ApiController] + 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; - public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage, + public QueueController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService, - QualityProfileService qualityProfileService) + QualityProfileService qualityProfileService, + ITrackedDownloadService trackedDownloadService, + IFailedDownloadService failedDownloadService, + IIgnoredDownloadService ignoredDownloadService, + IProvideDownloadClient downloadClientProvider) : base(broadcastSignalRMessage) { _queueService = queueService; _pendingReleaseService = pendingReleaseService; - GetResourcePaged = GetQueue; + _trackedDownloadService = trackedDownloadService; + _failedDownloadService = failedDownloadService; + _ignoredDownloadService = ignoredDownloadService; + _downloadClientProvider = downloadClientProvider; _qualityComparer = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty)); } - private PagingResource GetQueue(PagingResource pagingResource) + public override QueueResource GetResourceById(int id) { + throw new NotImplementedException(); + } + + [RestDeleteById] + public void RemoveAction(int id, bool removeFromClient = true, bool blacklist = false, bool skipReDownload = false) + { + var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload); + + if (trackedDownload != null) + { + _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); + } + } + + [HttpDelete("bulk")] + public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blacklist = false, [FromQuery] bool skipReDownload = false) + { + var trackedDownloadIds = new List(); + + foreach (var id in resource.Ids) + { + var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload); + + if (trackedDownload != null) + { + trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); + } + } + + _trackedDownloadService.StopTracking(trackedDownloadIds); + + return new object(); + } + + [HttpGet] + public PagingResource GetQueue(bool includeUnknownArtistItems = false, bool includeArtist = false, bool includeAlbum = false) + { + var pagingResource = Request.ReadPagingResourceFromRequest(); var pagingSpec = pagingResource.MapToPagingSpec("timeleft", SortDirection.Ascending); - var includeUnknownArtistItems = Request.GetBooleanQueryParameter("includeUnknownArtistItems"); - var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); - var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum"); - return ApplyToPage((spec) => GetQueue(spec, includeUnknownArtistItems), pagingSpec, (q) => MapToResource(q, includeArtist, includeAlbum)); + return pagingSpec.ApplyToPage((spec) => GetQueue(spec, includeUnknownArtistItems), (q) => MapToResource(q, includeArtist, includeAlbum)); } private PagingSpec GetQueue(PagingSpec pagingSpec, bool includeUnknownArtistItems) @@ -138,16 +191,83 @@ namespace Lidarr.Api.V1.Queue } } + private TrackedDownload Remove(int id, bool removeFromClient, bool blacklist, bool skipReDownload) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease != null) + { + _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.DownloadId, true); + } + + if (blacklist) + { + _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipReDownload); + } + + if (!removeFromClient && !blacklist) + { + 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 includeArtist, bool includeAlbum) { return queueItem.ToResource(includeArtist, includeAlbum); } + [NonAction] public void Handle(QueueUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); } + [NonAction] public void Handle(PendingReleasesUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); diff --git a/src/Lidarr.Api.V1/Queue/QueueDetailsModule.cs b/src/Lidarr.Api.V1/Queue/QueueDetailsController.cs similarity index 52% rename from src/Lidarr.Api.V1/Queue/QueueDetailsModule.cs rename to src/Lidarr.Api.V1/Queue/QueueDetailsController.cs index 569b1ec26..548954675 100644 --- a/src/Lidarr.Api.V1/Queue/QueueDetailsModule.cs +++ b/src/Lidarr.Api.V1/Queue/QueueDetailsController.cs @@ -2,7 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using Lidarr.Http; -using Lidarr.Http.Extensions; +using Lidarr.Http.REST; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Messaging.Events; @@ -11,55 +12,52 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Queue { - public class QueueDetailsModule : LidarrRestModuleWithSignalR, + [V1ApiController("queue/details")] + public class QueueDetailsController : RestControllerWithSignalR, IHandle, IHandle { private readonly IQueueService _queueService; private readonly IPendingReleaseService _pendingReleaseService; - public QueueDetailsModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) - : base(broadcastSignalRMessage, "queue/details") + public QueueDetailsController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage) { _queueService = queueService; _pendingReleaseService = pendingReleaseService; - GetResourceAll = GetQueue; } - private List GetQueue() + public override QueueResource GetResourceById(int id) + { + throw new NotImplementedException(); + } + + [HttpGet] + public List GetQueue(int? artistId, [FromQuery]List albumIds, bool includeArtist = false, bool includeAlbum = true) { - var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); - var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum", true); var queue = _queueService.GetQueue(); var pending = _pendingReleaseService.GetPendingQueue(); var fullQueue = queue.Concat(pending); - var artistIdQuery = Request.Query.ArtistId; - var albumIdsQuery = Request.Query.AlbumIds; - - if (artistIdQuery.HasValue) + if (artistId.HasValue) { - return fullQueue.Where(q => q.Artist?.Id == (int)artistIdQuery).ToResource(includeArtist, includeAlbum); + return fullQueue.Where(q => q.Artist?.Id == artistId.Value).ToResource(includeArtist, includeAlbum); } - if (albumIdsQuery.HasValue) + if (albumIds.Any()) { - string albumIdsValue = albumIdsQuery.Value.ToString(); - - var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => Convert.ToInt32(e)) - .ToList(); - return fullQueue.Where(q => q.Album != null && albumIds.Contains(q.Album.Id)).ToResource(includeArtist, includeAlbum); } return fullQueue.ToResource(includeArtist, includeAlbum); } + [NonAction] public void Handle(QueueUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); } + [NonAction] public void Handle(PendingReleasesUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); diff --git a/src/Lidarr.Api.V1/Queue/QueueStatusModule.cs b/src/Lidarr.Api.V1/Queue/QueueStatusController.cs similarity index 77% rename from src/Lidarr.Api.V1/Queue/QueueStatusModule.cs rename to src/Lidarr.Api.V1/Queue/QueueStatusController.cs index 319c811c0..1243f5b15 100644 --- a/src/Lidarr.Api.V1/Queue/QueueStatusModule.cs +++ b/src/Lidarr.Api.V1/Queue/QueueStatusController.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using Lidarr.Http; +using Lidarr.Http.REST; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.TPL; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download.Pending; @@ -11,30 +13,30 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Queue { - public class QueueStatusModule : LidarrRestModuleWithSignalR, + [V1ApiController("queue/status")] + public class QueueStatusController : RestControllerWithSignalR, IHandle, IHandle { private readonly IQueueService _queueService; private readonly IPendingReleaseService _pendingReleaseService; private readonly Debouncer _broadcastDebounce; - public QueueStatusModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) - : base(broadcastSignalRMessage, "queue/status") + public QueueStatusController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage) { _queueService = queueService; _pendingReleaseService = pendingReleaseService; _broadcastDebounce = new Debouncer(BroadcastChange, TimeSpan.FromSeconds(5)); - - Get("/", x => GetQueueStatusResponse()); } - private object GetQueueStatusResponse() + public override QueueStatusResource GetResourceById(int id) { - return GetQueueStatus(); + throw new NotImplementedException(); } - private QueueStatusResource GetQueueStatus() + [HttpGet] + public QueueStatusResource GetQueueStatus() { _broadcastDebounce.Pause(); @@ -62,11 +64,13 @@ namespace Lidarr.Api.V1.Queue BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); } + [NonAction] public void Handle(QueueUpdatedEvent message) { _broadcastDebounce.Execute(); } + [NonAction] public void Handle(PendingReleasesUpdatedEvent message) { _broadcastDebounce.Execute(); diff --git a/src/Lidarr.Api.V1/RemotePathMappings/RemotePathMappingModule.cs b/src/Lidarr.Api.V1/RemotePathMappings/RemotePathMappingController.cs similarity index 60% rename from src/Lidarr.Api.V1/RemotePathMappings/RemotePathMappingModule.cs rename to src/Lidarr.Api.V1/RemotePathMappings/RemotePathMappingController.cs index e167bc9dc..bb32b336a 100644 --- a/src/Lidarr.Api.V1/RemotePathMappings/RemotePathMappingModule.cs +++ b/src/Lidarr.Api.V1/RemotePathMappings/RemotePathMappingController.cs @@ -1,27 +1,25 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation; using Lidarr.Http; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation.Paths; namespace Lidarr.Api.V1.RemotePathMappings { - public class RemotePathMappingModule : LidarrRestModule + [V1ApiController] + public class RemotePathMappingController : RestController { private readonly IRemotePathMappingService _remotePathMappingService; - public RemotePathMappingModule(IRemotePathMappingService remotePathMappingService, + public RemotePathMappingController(IRemotePathMappingService remotePathMappingService, PathExistsValidator pathExistsValidator, MappedNetworkDriveValidator mappedNetworkDriveValidator) { _remotePathMappingService = remotePathMappingService; - GetResourceAll = GetMappings; - GetResourceById = GetMappingById; - CreateResource = CreateMapping; - DeleteResource = DeleteMapping; - UpdateResource = UpdateMapping; - SharedValidator.RuleFor(c => c.Host) .NotEmpty(); @@ -36,33 +34,37 @@ namespace Lidarr.Api.V1.RemotePathMappings .SetValidator(pathExistsValidator); } - private RemotePathMappingResource GetMappingById(int id) + public override RemotePathMappingResource GetResourceById(int id) { return _remotePathMappingService.Get(id).ToResource(); } - private int CreateMapping(RemotePathMappingResource resource) + [RestPostById] + public ActionResult CreateMapping(RemotePathMappingResource resource) { var model = resource.ToModel(); - return _remotePathMappingService.Add(model).Id; + return Created(_remotePathMappingService.Add(model).Id); } - private List GetMappings() + [HttpGet] + public List GetMappings() { return _remotePathMappingService.All().ToResource(); } - private void DeleteMapping(int id) + [RestDeleteById] + public void DeleteMapping(int id) { _remotePathMappingService.Remove(id); } - private void UpdateMapping(RemotePathMappingResource resource) + [RestPutById] + public ActionResult UpdateMapping(RemotePathMappingResource resource) { var mapping = resource.ToModel(); - _remotePathMappingService.Update(mapping); + return Accepted(_remotePathMappingService.Update(mapping)); } } } diff --git a/src/Lidarr.Api.V1/RootFolders/RootFolderModule.cs b/src/Lidarr.Api.V1/RootFolders/RootFolderController.cs similarity index 76% rename from src/Lidarr.Api.V1/RootFolders/RootFolderModule.cs rename to src/Lidarr.Api.V1/RootFolders/RootFolderController.cs index 731acf561..48584f3bb 100644 --- a/src/Lidarr.Api.V1/RootFolders/RootFolderModule.cs +++ b/src/Lidarr.Api.V1/RootFolders/RootFolderController.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using FluentValidation; using Lidarr.Http; using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; @@ -9,11 +11,12 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.RootFolders { - public class RootFolderModule : LidarrRestModuleWithSignalR + [V1ApiController] + public class RootFolderController : RestControllerWithSignalR { private readonly IRootFolderService _rootFolderService; - public RootFolderModule(IRootFolderService rootFolderService, + public RootFolderController(IRootFolderService rootFolderService, IBroadcastSignalRMessage signalRBroadcaster, RootFolderValidator rootFolderValidator, PathExistsValidator pathExistsValidator, @@ -27,12 +30,6 @@ namespace Lidarr.Api.V1.RootFolders { _rootFolderService = rootFolderService; - GetResourceAll = GetRootFolders; - GetResourceById = GetRootFolder; - CreateResource = CreateRootFolder; - UpdateResource = UpdateRootFolder; - DeleteResource = DeleteFolder; - SharedValidator.RuleFor(c => c.Path) .Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() @@ -55,19 +52,21 @@ namespace Lidarr.Api.V1.RootFolders .SetValidator(qualityProfileExistsValidator); } - private RootFolderResource GetRootFolder(int id) + public override RootFolderResource GetResourceById(int id) { return _rootFolderService.Get(id).ToResource(); } - private int CreateRootFolder(RootFolderResource rootFolderResource) + [RestPostById] + public ActionResult CreateRootFolder(RootFolderResource rootFolderResource) { var model = rootFolderResource.ToModel(); - return _rootFolderService.Add(model).Id; + return Created(_rootFolderService.Add(model).Id); } - private void UpdateRootFolder(RootFolderResource rootFolderResource) + [RestPutById] + public ActionResult UpdateRootFolder(RootFolderResource rootFolderResource) { var model = rootFolderResource.ToModel(); @@ -77,14 +76,18 @@ namespace Lidarr.Api.V1.RootFolders } _rootFolderService.Update(model); + + return Accepted(model.Id); } - private List GetRootFolders() + [HttpGet] + public List GetRootFolders() { return _rootFolderService.AllWithSpaceStats().ToResource(); } - private void DeleteFolder(int id) + [RestDeleteById] + public void DeleteFolder(int id) { _rootFolderService.Remove(id); } diff --git a/src/Lidarr.Api.V1/Search/SearchModule.cs b/src/Lidarr.Api.V1/Search/SearchController.cs similarity index 88% rename from src/Lidarr.Api.V1/Search/SearchModule.cs rename to src/Lidarr.Api.V1/Search/SearchController.cs index cb291e42c..35d42869c 100644 --- a/src/Lidarr.Api.V1/Search/SearchModule.cs +++ b/src/Lidarr.Api.V1/Search/SearchController.cs @@ -4,26 +4,26 @@ using System.Linq; using Lidarr.Api.V1.Albums; using Lidarr.Api.V1.Artist; using Lidarr.Http; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; namespace Lidarr.Api.V1.Search { - public class SearchModule : LidarrRestModule + [V1ApiController] + public class SearchController : Controller { private readonly ISearchForNewEntity _searchProxy; - public SearchModule(ISearchForNewEntity searchProxy) - : base("/search") + public SearchController(ISearchForNewEntity searchProxy) { _searchProxy = searchProxy; - Get("/", x => Search()); } - private object Search() + [HttpGet] + public object Search([FromQuery] string term) { - var searchResults = _searchProxy.SearchForNewEntity((string)Request.Query.term); + var searchResults = _searchProxy.SearchForNewEntity(term); return MapToResource(searchResults).ToList(); } diff --git a/src/Lidarr.Api.V1/System/Backup/BackupModule.cs b/src/Lidarr.Api.V1/System/Backup/BackupController.cs similarity index 84% rename from src/Lidarr.Api.V1/System/Backup/BackupModule.cs rename to src/Lidarr.Api.V1/System/Backup/BackupController.cs index 61d9a6fe6..f36229fd2 100644 --- a/src/Lidarr.Api.V1/System/Backup/BackupModule.cs +++ b/src/Lidarr.Api.V1/System/Backup/BackupController.cs @@ -3,6 +3,8 @@ using System.IO; using System.Linq; using Lidarr.Http; using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Crypto; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -11,7 +13,8 @@ using NzbDrone.Core.Backup; namespace Lidarr.Api.V1.System.Backup { - public class BackupModule : LidarrRestModule + [V1ApiController("system/backup")] + public class BackupController : Controller { private readonly IBackupService _backupService; private readonly IAppFolderInfo _appFolderInfo; @@ -19,21 +22,16 @@ namespace Lidarr.Api.V1.System.Backup private static readonly List ValidExtensions = new List { ".zip", ".db", ".xml" }; - public BackupModule(IBackupService backupService, + public BackupController(IBackupService backupService, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) - : base("system/backup") { _backupService = backupService; _appFolderInfo = appFolderInfo; _diskProvider = diskProvider; - GetResourceAll = GetBackupFiles; - DeleteResource = DeleteBackup; - - Post(@"/restore/(?[\d]{1,10})", x => Restore((int)x.Id)); - Post("/restore/upload", x => UploadAndRestore()); } + [HttpGet] public List GetBackupFiles() { var backups = _backupService.GetBackups(); @@ -50,7 +48,8 @@ namespace Lidarr.Api.V1.System.Backup .ToList(); } - private void DeleteBackup(int id) + [RestDeleteById] + public void DeleteBackup(int id) { var backup = GetBackup(id); var path = GetBackupPath(backup); @@ -63,6 +62,7 @@ namespace Lidarr.Api.V1.System.Backup _diskProvider.DeleteFile(path); } + [HttpPost("restore/{id:int}")] public object Restore(int id) { var backup = GetBackup(id); @@ -82,9 +82,10 @@ namespace Lidarr.Api.V1.System.Backup }; } + [HttpPost("restore/upload")] public object UploadAndRestore() { - var files = Context.Request.Files.ToList(); + var files = Request.Form.Files; if (files.Empty()) { @@ -92,7 +93,7 @@ namespace Lidarr.Api.V1.System.Backup } var file = files.First(); - var extension = Path.GetExtension(file.Name); + var extension = Path.GetExtension(file.FileName); if (!ValidExtensions.Contains(extension)) { @@ -101,7 +102,7 @@ namespace Lidarr.Api.V1.System.Backup var path = Path.Combine(_appFolderInfo.TempFolder, $"lidarr_backup_restore{extension}"); - _diskProvider.SaveStream(file.Value, path); + _diskProvider.SaveStream(file.OpenReadStream(), path); _backupService.Restore(path); // Cleanup restored file diff --git a/src/Lidarr.Api.V1/System/SystemModule.cs b/src/Lidarr.Api.V1/System/SystemController.cs similarity index 61% rename from src/Lidarr.Api.V1/System/SystemModule.cs rename to src/Lidarr.Api.V1/System/SystemController.cs index c2195151b..e53b8cda3 100644 --- a/src/Lidarr.Api.V1/System/SystemModule.cs +++ b/src/Lidarr.Api.V1/System/SystemController.cs @@ -1,5 +1,10 @@ +using System.IO; using System.Threading.Tasks; -using Nancy.Routing; +using Lidarr.Http; +using Lidarr.Http.Validation; +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; @@ -8,45 +13,48 @@ using NzbDrone.Core.Lifecycle; namespace Lidarr.Api.V1.System { - public class SystemModule : LidarrV1Module + [V1ApiController] + public class SystemController : Controller { private readonly IAppFolderInfo _appFolderInfo; private readonly IRuntimeInfo _runtimeInfo; private readonly IPlatformInfo _platformInfo; private readonly IOsInfo _osInfo; - private readonly IRouteCacheProvider _routeCacheProvider; 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 SystemModule(IAppFolderInfo appFolderInfo, - IRuntimeInfo runtimeInfo, - IPlatformInfo platformInfo, - IOsInfo osInfo, - IRouteCacheProvider routeCacheProvider, - IConfigFileProvider configFileProvider, - IMainDatabase database, - ILifecycleService lifecycleService, - IDeploymentInfoProvider deploymentInfoProvider) - : base("system") + 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; - _routeCacheProvider = routeCacheProvider; _configFileProvider = configFileProvider; _database = database; _lifecycleService = lifecycleService; _deploymentInfoProvider = deploymentInfoProvider; - Get("/status", x => GetStatus()); - Get("/routes", x => GetRoutes()); - Post("/shutdown", x => Shutdown()); - Post("/restart", x => Restart()); + _endpointData = endpoints; + _graphWriter = graphWriter; + _detector = detector; } - private object GetStatus() + [HttpGet("status")] + public object GetStatus() { return new { @@ -81,18 +89,32 @@ namespace Lidarr.Api.V1.System }; } - private object GetRoutes() + [HttpGet("routes")] + public IActionResult GetRoutes() { - return _routeCacheProvider.GetCache().Values; + 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); } - private object Shutdown() + [HttpPost("shutdown")] + public object Shutdown() { Task.Factory.StartNew(() => _lifecycleService.Shutdown()); return new { ShuttingDown = true }; } - private object Restart() + [HttpPost("restart")] + public object Restart() { Task.Factory.StartNew(() => _lifecycleService.Restart()); return new { Restarting = true }; diff --git a/src/Lidarr.Api.V1/System/Tasks/TaskModule.cs b/src/Lidarr.Api.V1/System/Tasks/TaskController.cs similarity index 76% rename from src/Lidarr.Api.V1/System/Tasks/TaskModule.cs rename to src/Lidarr.Api.V1/System/Tasks/TaskController.cs index 61a8c6a7f..6073b5768 100644 --- a/src/Lidarr.Api.V1/System/Tasks/TaskModule.cs +++ b/src/Lidarr.Api.V1/System/Tasks/TaskController.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Linq; using Lidarr.Http; +using Lidarr.Http.REST; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Jobs; @@ -9,19 +11,19 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.System.Tasks { - public class TaskModule : LidarrRestModuleWithSignalR, IHandle + [V1ApiController("system/task")] + public class TaskController : RestControllerWithSignalR, IHandle { private readonly ITaskManager _taskManager; - public TaskModule(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage) - : base(broadcastSignalRMessage, "system/task") + public TaskController(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage) + : base(broadcastSignalRMessage) { _taskManager = taskManager; - GetResourceAll = GetAll; - GetResourceById = GetTask; } - private List GetAll() + [HttpGet] + public List GetAll() { return _taskManager.GetAll() .Select(ConvertToResource) @@ -29,7 +31,7 @@ namespace Lidarr.Api.V1.System.Tasks .ToList(); } - private TaskResource GetTask(int id) + public override TaskResource GetResourceById(int id) { var task = _taskManager.GetAll() .SingleOrDefault(t => t.Id == id); @@ -58,6 +60,7 @@ namespace Lidarr.Api.V1.System.Tasks }; } + [NonAction] public void Handle(CommandExecutedEvent message) { BroadcastResourceChange(ModelAction.Sync); diff --git a/src/Lidarr.Api.V1/Tags/TagModule.cs b/src/Lidarr.Api.V1/Tags/TagController.cs similarity index 51% rename from src/Lidarr.Api.V1/Tags/TagModule.cs rename to src/Lidarr.Api.V1/Tags/TagController.cs index c0690c1ca..3458511e5 100644 --- a/src/Lidarr.Api.V1/Tags/TagModule.cs +++ b/src/Lidarr.Api.V1/Tags/TagController.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; using Lidarr.Http; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tags; @@ -7,48 +10,49 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Tags { - public class TagModule : LidarrRestModuleWithSignalR, IHandle + [V1ApiController] + public class TagController : RestControllerWithSignalR, IHandle { private readonly ITagService _tagService; - public TagModule(IBroadcastSignalRMessage signalRBroadcaster, + public TagController(IBroadcastSignalRMessage signalRBroadcaster, ITagService tagService) : base(signalRBroadcaster) { _tagService = tagService; - - GetResourceById = GetTag; - GetResourceAll = GetAll; - CreateResource = Create; - UpdateResource = Update; - DeleteResource = DeleteTag; } - private TagResource GetTag(int id) + public override TagResource GetResourceById(int id) { return _tagService.GetTag(id).ToResource(); } - private List GetAll() + [HttpGet] + public List GetAll() { return _tagService.All().ToResource(); } - private int Create(TagResource resource) + [RestPostById] + public ActionResult Create(TagResource resource) { - return _tagService.Add(resource.ToModel()).Id; + return Created(_tagService.Add(resource.ToModel()).Id); } - private void Update(TagResource resource) + [RestPutById] + public ActionResult Update(TagResource resource) { _tagService.Update(resource.ToModel()); + return Accepted(resource.Id); } - private void DeleteTag(int id) + [RestDeleteById] + public void DeleteTag(int id) { _tagService.Delete(id); } + [NonAction] public void Handle(TagsUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); diff --git a/src/Lidarr.Api.V1/Tags/TagDetailsModule.cs b/src/Lidarr.Api.V1/Tags/TagDetailsController.cs similarity index 53% rename from src/Lidarr.Api.V1/Tags/TagDetailsModule.cs rename to src/Lidarr.Api.V1/Tags/TagDetailsController.cs index e8a50d8aa..4016d179d 100644 --- a/src/Lidarr.Api.V1/Tags/TagDetailsModule.cs +++ b/src/Lidarr.Api.V1/Tags/TagDetailsController.cs @@ -1,28 +1,28 @@ using System.Collections.Generic; using Lidarr.Http; +using Lidarr.Http.REST; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Tags; namespace Lidarr.Api.V1.Tags { - public class TagDetailsModule : LidarrRestModule + [V1ApiController("tag/detail")] + public class TagDetailsController : RestController { private readonly ITagService _tagService; - public TagDetailsModule(ITagService tagService) - : base("/tag/detail") + public TagDetailsController(ITagService tagService) { _tagService = tagService; - - GetResourceById = GetTagDetails; - GetResourceAll = GetAll; } - private TagDetailsResource GetTagDetails(int id) + public override TagDetailsResource GetResourceById(int id) { return _tagService.Details(id).ToResource(); } - private List GetAll() + [HttpGet] + public List GetAll() { var tags = _tagService.Details().ToResource(); diff --git a/src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs b/src/Lidarr.Api.V1/TrackFiles/TrackFileController.cs similarity index 66% rename from src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs rename to src/Lidarr.Api.V1/TrackFiles/TrackFileController.cs index e924744e2..0cc4e6ccf 100644 --- a/src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs +++ b/src/Lidarr.Api.V1/TrackFiles/TrackFileController.cs @@ -1,9 +1,9 @@ -using System; using System.Collections.Generic; using System.Linq; using Lidarr.Http; -using Lidarr.Http.Extensions; -using Nancy; +using Lidarr.Http.REST; +using Lidarr.Http.REST.Attributes; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Exceptions; @@ -12,11 +12,13 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; using NzbDrone.SignalR; +using BadRequestException = Lidarr.Http.REST.BadRequestException; using HttpStatusCode = System.Net.HttpStatusCode; namespace Lidarr.Api.V1.TrackFiles { - public class TrackFileModule : LidarrRestModuleWithSignalR, + [V1ApiController] + public class TrackFileController : RestControllerWithSignalR, IHandle, IHandle { @@ -27,7 +29,7 @@ namespace Lidarr.Api.V1.TrackFiles private readonly IAlbumService _albumService; private readonly IUpgradableSpecification _upgradableSpecification; - public TrackFileModule(IBroadcastSignalRMessage signalRBroadcaster, + public TrackFileController(IBroadcastSignalRMessage signalRBroadcaster, IMediaFileService mediaFileService, IDeleteMediaFiles mediaFileDeletionService, IAudioTagService audioTagService, @@ -42,14 +44,6 @@ namespace Lidarr.Api.V1.TrackFiles _artistService = artistService; _albumService = albumService; _upgradableSpecification = upgradableSpecification; - - GetResourceById = GetTrackFile; - GetResourceAll = GetTrackFiles; - UpdateResource = SetQuality; - DeleteResource = DeleteTrackFile; - - Put("/editor", trackFiles => SetQuality()); - Delete("/bulk", trackFiles => DeleteTrackFiles()); } private TrackFileResource MapToResource(TrackFile trackFile) @@ -64,47 +58,36 @@ namespace Lidarr.Api.V1.TrackFiles } } - private TrackFileResource GetTrackFile(int id) + public override TrackFileResource GetResourceById(int id) { var resource = MapToResource(_mediaFileService.Get(id)); resource.AudioTags = _audioTagService.ReadTags(resource.Path); return resource; } - private List GetTrackFiles() + [HttpGet] + public List GetTrackFiles(int? artistId, [FromQuery] List trackFileIds, [FromQuery(Name = "albumId")] List albumIds, bool? unmapped) { - var artistIdQuery = Request.Query.ArtistId; - var trackFileIdsQuery = Request.Query.TrackFileIds; - var albumIdQuery = Request.Query.AlbumId; - var unmappedQuery = Request.Query.Unmapped; - - if (!artistIdQuery.HasValue && !trackFileIdsQuery.HasValue && !albumIdQuery.HasValue && !unmappedQuery.HasValue) + if (!artistId.HasValue && !trackFileIds.Any() && !albumIds.Any() && !unmapped.HasValue) { - throw new Lidarr.Http.REST.BadRequestException("artistId, albumId, trackFileIds or unmapped must be provided"); + throw new BadRequestException("artistId, albumId, trackFileIds or unmapped must be provided"); } - if (unmappedQuery.HasValue && Convert.ToBoolean(unmappedQuery.Value)) + if (unmapped.HasValue && unmapped.Value) { var files = _mediaFileService.GetUnmappedFiles(); return files.ConvertAll(f => MapToResource(f)); } - if (artistIdQuery.HasValue && !albumIdQuery.HasValue) + if (artistId.HasValue && !albumIds.Any()) { - int artistId = Convert.ToInt32(artistIdQuery.Value); - var artist = _artistService.GetArtist(artistId); + var artist = _artistService.GetArtist(artistId.Value); - return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _upgradableSpecification)); + return _mediaFileService.GetFilesByArtist(artistId.Value).ConvertAll(f => f.ToResource(artist, _upgradableSpecification)); } - if (albumIdQuery.HasValue) + if (albumIds.Any()) { - string albumIdValue = albumIdQuery.Value.ToString(); - - var albumIds = albumIdValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => Convert.ToInt32(e)) - .ToList(); - var result = new List(); foreach (var albumId in albumIds) { @@ -117,28 +100,24 @@ namespace Lidarr.Api.V1.TrackFiles } else { - string trackFileIdsValue = trackFileIdsQuery.Value.ToString(); - - var trackFileIds = trackFileIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => Convert.ToInt32(e)) - .ToList(); - // trackfiles will come back with the artist already populated var trackFiles = _mediaFileService.Get(trackFileIds); return trackFiles.ConvertAll(e => MapToResource(e)); } } - private void SetQuality(TrackFileResource trackFileResource) + [RestPutById] + public ActionResult SetQuality([FromBody] TrackFileResource trackFileResource) { var trackFile = _mediaFileService.Get(trackFileResource.Id); trackFile.Quality = trackFileResource.Quality; _mediaFileService.Update(trackFile); + return Accepted(trackFile.Id); } - private object SetQuality() + [HttpPut("editor")] + public IActionResult SetQuality([FromBody] TrackFileListResource resource) { - var resource = Request.Body.FromJson(); var trackFiles = _mediaFileService.Get(resource.TrackFileIds); foreach (var trackFile in trackFiles) @@ -151,11 +130,11 @@ namespace Lidarr.Api.V1.TrackFiles _mediaFileService.Update(trackFiles); - return ResponseWithCode(trackFiles.ConvertAll(f => f.ToResource(trackFiles.First().Artist.Value, _upgradableSpecification)), - Nancy.HttpStatusCode.Accepted); + return Accepted(trackFiles.ConvertAll(f => f.ToResource(trackFiles.First().Artist.Value, _upgradableSpecification))); } - private void DeleteTrackFile(int id) + [RestDeleteById] + public void DeleteTrackFile(int id) { var trackFile = _mediaFileService.Get(id); @@ -174,9 +153,9 @@ namespace Lidarr.Api.V1.TrackFiles } } - private object DeleteTrackFiles() + [HttpDelete("bulk")] + public IActionResult DeleteTrackFiles([FromBody] TrackFileListResource resource) { - var resource = Request.Body.FromJson(); var trackFiles = _mediaFileService.Get(resource.TrackFileIds); var artist = trackFiles.First().Artist.Value; @@ -185,14 +164,16 @@ namespace Lidarr.Api.V1.TrackFiles _mediaFileDeletionService.DeleteTrackFile(artist, trackFile); } - return new object(); + return Ok(); } + [NonAction] public void Handle(TrackFileAddedEvent message) { BroadcastResourceChange(ModelAction.Updated, MapToResource(message.TrackFile)); } + [NonAction] public void Handle(TrackFileDeletedEvent message) { BroadcastResourceChange(ModelAction.Deleted, MapToResource(message.TrackFile)); diff --git a/src/Lidarr.Api.V1/Tracks/RenameTrackController.cs b/src/Lidarr.Api.V1/Tracks/RenameTrackController.cs new file mode 100644 index 000000000..aba582c1c --- /dev/null +++ b/src/Lidarr.Api.V1/Tracks/RenameTrackController.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.MediaFiles; + +namespace Lidarr.Api.V1.Tracks +{ + [V1ApiController("rename")] + public class RenameTrackController : Controller + { + private readonly IRenameTrackFileService _renameTrackFileService; + + public RenameTrackController(IRenameTrackFileService renameTrackFileService) + { + _renameTrackFileService = renameTrackFileService; + } + + [HttpGet] + public List GetTracks(int artistId, int? albumId) + { + if (albumId.HasValue) + { + return _renameTrackFileService.GetRenamePreviews(artistId, albumId.Value).ToResource(); + } + + return _renameTrackFileService.GetRenamePreviews(artistId).ToResource(); + } + } +} diff --git a/src/Lidarr.Api.V1/Tracks/RenameTrackModule.cs b/src/Lidarr.Api.V1/Tracks/RenameTrackModule.cs deleted file mode 100644 index 5ecaa239f..000000000 --- a/src/Lidarr.Api.V1/Tracks/RenameTrackModule.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using Lidarr.Http; -using Lidarr.Http.REST; -using NzbDrone.Core.MediaFiles; - -namespace Lidarr.Api.V1.Tracks -{ - public class RenameTrackModule : LidarrRestModule - { - private readonly IRenameTrackFileService _renameTrackFileService; - - public RenameTrackModule(IRenameTrackFileService renameTrackFileService) - : base("rename") - { - _renameTrackFileService = renameTrackFileService; - - GetResourceAll = GetTracks; - } - - private List GetTracks() - { - int artistId; - - if (Request.Query.ArtistId.HasValue) - { - artistId = (int)Request.Query.ArtistId; - } - else - { - throw new BadRequestException("artistId is missing"); - } - - if (Request.Query.albumId.HasValue) - { - var albumId = (int)Request.Query.albumId; - return _renameTrackFileService.GetRenamePreviews(artistId, albumId).ToResource(); - } - - return _renameTrackFileService.GetRenamePreviews(artistId).ToResource(); - } - } -} diff --git a/src/Lidarr.Api.V1/Tracks/RetagTrackController.cs b/src/Lidarr.Api.V1/Tracks/RetagTrackController.cs new file mode 100644 index 000000000..ed7c5bbdf --- /dev/null +++ b/src/Lidarr.Api.V1/Tracks/RetagTrackController.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; +using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.MediaFiles; + +namespace Lidarr.Api.V1.Tracks +{ + [V1ApiController("retag")] + public class RetagTrackController : Controller + { + private readonly IAudioTagService _audioTagService; + + public RetagTrackController(IAudioTagService audioTagService) + { + _audioTagService = audioTagService; + } + + [HttpGet] + public List GetTracks(int artistId, int? albumId) + { + if (albumId.HasValue) + { + return _audioTagService.GetRetagPreviewsByAlbum(albumId.Value).Where(x => x.Changes.Any()).ToResource(); + } + + return _audioTagService.GetRetagPreviewsByArtist(artistId).Where(x => x.Changes.Any()).ToResource(); + } + } +} diff --git a/src/Lidarr.Api.V1/Tracks/RetagTrackModule.cs b/src/Lidarr.Api.V1/Tracks/RetagTrackModule.cs deleted file mode 100644 index 6f1c00d27..000000000 --- a/src/Lidarr.Api.V1/Tracks/RetagTrackModule.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Lidarr.Http; -using Lidarr.Http.REST; -using NzbDrone.Core.MediaFiles; - -namespace Lidarr.Api.V1.Tracks -{ - public class RetagTrackModule : LidarrRestModule - { - private readonly IAudioTagService _audioTagService; - - public RetagTrackModule(IAudioTagService audioTagService) - : base("retag") - { - _audioTagService = audioTagService; - - GetResourceAll = GetTracks; - } - - private List GetTracks() - { - if (Request.Query.albumId.HasValue) - { - var albumId = (int)Request.Query.albumId; - return _audioTagService.GetRetagPreviewsByAlbum(albumId).Where(x => x.Changes.Any()).ToResource(); - } - else if (Request.Query.ArtistId.HasValue) - { - var artistId = (int)Request.Query.ArtistId; - return _audioTagService.GetRetagPreviewsByArtist(artistId).Where(x => x.Changes.Any()).ToResource(); - } - else - { - throw new BadRequestException("One of artistId or albumId must be specified"); - } - } - } -} diff --git a/src/Lidarr.Api.V1/Tracks/TrackController.cs b/src/Lidarr.Api.V1/Tracks/TrackController.cs new file mode 100644 index 000000000..b9766a98b --- /dev/null +++ b/src/Lidarr.Api.V1/Tracks/TrackController.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using Lidarr.Http; +using Lidarr.Http.REST; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; + +namespace Lidarr.Api.V1.Tracks +{ + [V1ApiController] + public class TrackController : TrackControllerWithSignalR + { + public TrackController(IArtistService artistService, + ITrackService trackService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(trackService, artistService, upgradableSpecification, signalRBroadcaster) + { + } + + [HttpGet] + public List GetTracks([FromQuery]int? artistId, + [FromQuery]int? albumId, + [FromQuery]int? albumReleaseId, + [FromQuery]List trackIds) + { + if (!artistId.HasValue && !trackIds.Any() && !albumId.HasValue && !albumReleaseId.HasValue) + { + throw new BadRequestException("One of artistId, albumId, albumReleaseId or trackIds must be provided"); + } + + if (artistId.HasValue && !albumId.HasValue) + { + return MapToResource(_trackService.GetTracksByArtist(artistId.Value), false, false); + } + + if (albumReleaseId.HasValue) + { + return MapToResource(_trackService.GetTracksByRelease(albumReleaseId.Value), false, false); + } + + if (albumId.HasValue) + { + return MapToResource(_trackService.GetTracksByAlbum(albumId.Value), false, false); + } + + return MapToResource(_trackService.GetTracks(trackIds), false, false); + } + } +} diff --git a/src/Lidarr.Api.V1/Tracks/TrackModuleWithSignalR.cs b/src/Lidarr.Api.V1/Tracks/TrackControllerWithSignalR.cs similarity index 79% rename from src/Lidarr.Api.V1/Tracks/TrackModuleWithSignalR.cs rename to src/Lidarr.Api.V1/Tracks/TrackControllerWithSignalR.cs index 906835231..066f01cbf 100644 --- a/src/Lidarr.Api.V1/Tracks/TrackModuleWithSignalR.cs +++ b/src/Lidarr.Api.V1/Tracks/TrackControllerWithSignalR.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using Lidarr.Api.V1.Artist; using Lidarr.Api.V1.TrackFiles; -using Lidarr.Http; +using Lidarr.Http.REST; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.MediaFiles.Events; @@ -11,7 +12,7 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Tracks { - public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR, + public abstract class TrackControllerWithSignalR : RestControllerWithSignalR, IHandle, IHandle { @@ -19,7 +20,7 @@ namespace Lidarr.Api.V1.Tracks protected readonly IArtistService _artistService; protected readonly IUpgradableSpecification _upgradableSpecification; - protected TrackModuleWithSignalR(ITrackService trackService, + protected TrackControllerWithSignalR(ITrackService trackService, IArtistService artistService, IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) @@ -28,25 +29,9 @@ namespace Lidarr.Api.V1.Tracks _trackService = trackService; _artistService = artistService; _upgradableSpecification = upgradableSpecification; - - GetResourceById = GetTrack; - } - - protected TrackModuleWithSignalR(ITrackService trackService, - IArtistService artistService, - IUpgradableSpecification upgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster, - string resource) - : base(signalRBroadcaster, resource) - { - _trackService = trackService; - _artistService = artistService; - _upgradableSpecification = upgradableSpecification; - - GetResourceById = GetTrack; } - protected TrackResource GetTrack(int id) + public override TrackResource GetResourceById(int id) { var track = _trackService.GetTrack(id); var resource = MapToResource(track, true, true); @@ -103,6 +88,7 @@ namespace Lidarr.Api.V1.Tracks return result; } + [NonAction] public void Handle(TrackImportedEvent message) { foreach (var track in message.TrackInfo.Tracks) @@ -112,6 +98,7 @@ namespace Lidarr.Api.V1.Tracks } } + [NonAction] public void Handle(TrackFileDeletedEvent message) { foreach (var track in message.TrackFile.Tracks.Value) diff --git a/src/Lidarr.Api.V1/Tracks/TrackModule.cs b/src/Lidarr.Api.V1/Tracks/TrackModule.cs deleted file mode 100644 index 911a25738..000000000 --- a/src/Lidarr.Api.V1/Tracks/TrackModule.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Lidarr.Http.REST; -using Nancy; -using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Music; -using NzbDrone.SignalR; - -namespace Lidarr.Api.V1.Tracks -{ - public class TrackModule : TrackModuleWithSignalR - { - public TrackModule(IArtistService artistService, - ITrackService trackService, - IUpgradableSpecification upgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(trackService, artistService, upgradableSpecification, signalRBroadcaster) - { - GetResourceAll = GetTracks; - } - - private List GetTracks() - { - var artistIdQuery = Request.Query.ArtistId; - var albumIdQuery = Request.Query.AlbumId; - var albumReleaseIdQuery = Request.Query.AlbumReleaseId; - var trackIdsQuery = Request.Query.TrackIds; - - if (!artistIdQuery.HasValue && !trackIdsQuery.HasValue && !albumIdQuery.HasValue && !albumReleaseIdQuery.HasValue) - { - throw new BadRequestException("One of artistId, albumId, albumReleaseId or trackIds must be provided"); - } - - if (artistIdQuery.HasValue && !albumIdQuery.HasValue) - { - int artistId = Convert.ToInt32(artistIdQuery.Value); - - return MapToResource(_trackService.GetTracksByArtist(artistId), false, false); - } - - if (albumReleaseIdQuery.HasValue) - { - int releaseId = Convert.ToInt32(albumReleaseIdQuery.Value); - - return MapToResource(_trackService.GetTracksByRelease(releaseId), false, false); - } - - if (albumIdQuery.HasValue) - { - int albumId = Convert.ToInt32(albumIdQuery.Value); - - return MapToResource(_trackService.GetTracksByAlbum(albumId), false, false); - } - - string trackIdsValue = trackIdsQuery.Value.ToString(); - - var trackIds = trackIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => Convert.ToInt32(e)) - .ToList(); - - return MapToResource(_trackService.GetTracks(trackIds), false, false); - } - } -} diff --git a/src/Lidarr.Api.V1/Update/UpdateModule.cs b/src/Lidarr.Api.V1/Update/UpdateController.cs similarity index 79% rename from src/Lidarr.Api.V1/Update/UpdateModule.cs rename to src/Lidarr.Api.V1/Update/UpdateController.cs index a878ae0d9..98b2be985 100644 --- a/src/Lidarr.Api.V1/Update/UpdateModule.cs +++ b/src/Lidarr.Api.V1/Update/UpdateController.cs @@ -1,22 +1,24 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Lidarr.Http; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Update; namespace Lidarr.Api.V1.Update { - public class UpdateModule : LidarrRestModule + [V1ApiController] + public class UpdateController : Controller { private readonly IRecentUpdateProvider _recentUpdateProvider; - public UpdateModule(IRecentUpdateProvider recentUpdateProvider) + public UpdateController(IRecentUpdateProvider recentUpdateProvider) { _recentUpdateProvider = recentUpdateProvider; - GetResourceAll = GetRecentUpdates; } - private List GetRecentUpdates() + [HttpGet] + public List GetRecentUpdates() { var resources = _recentUpdateProvider.GetRecentUpdatePackages() .OrderByDescending(u => u.Version) diff --git a/src/Lidarr.Api.V1/Wanted/CutoffModule.cs b/src/Lidarr.Api.V1/Wanted/CutoffController.cs similarity index 70% rename from src/Lidarr.Api.V1/Wanted/CutoffModule.cs rename to src/Lidarr.Api.V1/Wanted/CutoffController.cs index 4ea2d2146..5bec60323 100644 --- a/src/Lidarr.Api.V1/Wanted/CutoffModule.cs +++ b/src/Lidarr.Api.V1/Wanted/CutoffController.cs @@ -2,6 +2,7 @@ using System.Linq; using Lidarr.Api.V1.Albums; using Lidarr.Http; using Lidarr.Http.Extensions; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.ArtistStats; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine.Specifications; @@ -11,24 +12,26 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Wanted { - public class CutoffModule : AlbumModuleWithSignalR + [V1ApiController("wanted/cutoff")] + public class CutoffController : AlbumControllerWithSignalR { private readonly IAlbumCutoffService _albumCutoffService; - public CutoffModule(IAlbumCutoffService albumCutoffService, + public CutoffController(IAlbumCutoffService albumCutoffService, IAlbumService albumService, IArtistStatisticsService artistStatisticsService, IMapCoversToLocal coverMapper, IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) - : base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster, "wanted/cutoff") + : base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster) { _albumCutoffService = albumCutoffService; - GetResourcePaged = GetCutoffUnmetAlbums; } - private PagingResource GetCutoffUnmetAlbums(PagingResource pagingResource) + [HttpGet] + public PagingResource GetCutoffUnmetAlbums(bool includeArtist = false) { + var pagingResource = Request.ReadPagingResourceFromRequest(); var pagingSpec = new PagingSpec { Page = pagingResource.Page, @@ -37,7 +40,6 @@ namespace Lidarr.Api.V1.Wanted SortDirection = pagingResource.SortDirection }; - var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); var filter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); if (filter != null && filter.Value == "false") @@ -49,9 +51,7 @@ namespace Lidarr.Api.V1.Wanted pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Artist.Value.Monitored == true); } - var resource = ApplyToPage(_albumCutoffService.AlbumsWhereCutoffUnmet, pagingSpec, v => MapToResource(v, includeArtist)); - - return resource; + return pagingSpec.ApplyToPage(_albumCutoffService.AlbumsWhereCutoffUnmet, v => MapToResource(v, includeArtist)); } } } diff --git a/src/Lidarr.Api.V1/Wanted/MissingModule.cs b/src/Lidarr.Api.V1/Wanted/MissingController.cs similarity index 69% rename from src/Lidarr.Api.V1/Wanted/MissingModule.cs rename to src/Lidarr.Api.V1/Wanted/MissingController.cs index 5574c58dd..7e9e8599e 100644 --- a/src/Lidarr.Api.V1/Wanted/MissingModule.cs +++ b/src/Lidarr.Api.V1/Wanted/MissingController.cs @@ -2,6 +2,7 @@ using System.Linq; using Lidarr.Api.V1.Albums; using Lidarr.Http; using Lidarr.Http.Extensions; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.ArtistStats; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine.Specifications; @@ -11,20 +12,22 @@ using NzbDrone.SignalR; namespace Lidarr.Api.V1.Wanted { - public class MissingModule : AlbumModuleWithSignalR + [V1ApiController("wanted/missing")] + public class MissingController : AlbumControllerWithSignalR { - public MissingModule(IAlbumService albumService, + public MissingController(IAlbumService albumService, IArtistStatisticsService artistStatisticsService, IMapCoversToLocal coverMapper, IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) - : base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster, "wanted/missing") + : base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster) { - GetResourcePaged = GetMissingAlbums; } - private PagingResource GetMissingAlbums(PagingResource pagingResource) + [HttpGet] + public PagingResource GetMissingAlbums(bool includeArtist = false) { + var pagingResource = Request.ReadPagingResourceFromRequest(); var pagingSpec = new PagingSpec { Page = pagingResource.Page, @@ -33,7 +36,6 @@ namespace Lidarr.Api.V1.Wanted SortDirection = pagingResource.SortDirection }; - var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); var monitoredFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); if (monitoredFilter != null && monitoredFilter.Value == "false") @@ -45,9 +47,7 @@ namespace Lidarr.Api.V1.Wanted pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Artist.Value.Monitored == true); } - var resource = ApplyToPage(_albumService.AlbumsWithoutFiles, pagingSpec, v => MapToResource(v, includeArtist)); - - return resource; + return pagingSpec.ApplyToPage(_albumService.AlbumsWithoutFiles, v => MapToResource(v, includeArtist)); } } } diff --git a/src/Lidarr.Http/Authentication/ApiKeyAuthenticationHandler.cs b/src/Lidarr.Http/Authentication/ApiKeyAuthenticationHandler.cs new file mode 100644 index 000000000..edeb80a94 --- /dev/null +++ b/src/Lidarr.Http/Authentication/ApiKeyAuthenticationHandler.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Lidarr.Http.Authentication +{ + public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions + { + public const string DefaultScheme = "API Key"; + public string Scheme => DefaultScheme; + public string AuthenticationType = DefaultScheme; + + public string HeaderName { get; set; } + public string QueryName { get; set; } + public string ApiKey { get; set; } + } + + public class ApiKeyAuthenticationHandler : AuthenticationHandler + { + public ApiKeyAuthenticationHandler(IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + private string ParseApiKey() + { + // Try query parameter + if (Request.Query.TryGetValue(Options.QueryName, out var value)) + { + return value.FirstOrDefault(); + } + + // No ApiKey query parameter found try headers + if (Request.Headers.TryGetValue(Options.HeaderName, out var headerValue)) + { + return headerValue.FirstOrDefault(); + } + + return Request.Headers["Authorization"].FirstOrDefault()?.Replace("Bearer ", ""); + } + + protected override Task HandleAuthenticateAsync() + { + var providedApiKey = ParseApiKey(); + + if (string.IsNullOrWhiteSpace(providedApiKey)) + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + + if (Options.ApiKey == providedApiKey) + { + var claims = new List + { + new Claim("ApiKey", "true") + }; + + var identity = new ClaimsIdentity(claims, Options.AuthenticationType); + var identities = new List { identity }; + var principal = new ClaimsPrincipal(identities); + var ticket = new AuthenticationTicket(principal, Options.Scheme); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } + + return Task.FromResult(AuthenticateResult.NoResult()); + } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + Response.StatusCode = 401; + return Task.CompletedTask; + } + + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + { + Response.StatusCode = 403; + return Task.CompletedTask; + } + } +} diff --git a/src/Lidarr.Http/Authentication/AuthenticationBuilderExtensions.cs b/src/Lidarr.Http/Authentication/AuthenticationBuilderExtensions.cs new file mode 100644 index 000000000..13bf22f95 --- /dev/null +++ b/src/Lidarr.Http/Authentication/AuthenticationBuilderExtensions.cs @@ -0,0 +1,65 @@ +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Http.Authentication +{ + public static class AuthenticationBuilderExtensions + { + public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder authenticationBuilder, string name, Action options) + { + return authenticationBuilder.AddScheme(name, options); + } + + public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder authenticationBuilder) + { + return authenticationBuilder.AddScheme(AuthenticationType.Basic.ToString(), options => { }); + } + + public static AuthenticationBuilder AddNoAuthentication(this AuthenticationBuilder authenticationBuilder) + { + return authenticationBuilder.AddScheme(AuthenticationType.None.ToString(), options => { }); + } + + public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services, IConfigFileProvider config) + { + var authBuilder = services.AddAuthentication(config.AuthenticationMethod.ToString()); + + if (config.AuthenticationMethod == AuthenticationType.Basic) + { + authBuilder.AddBasicAuthentication(); + } + else if (config.AuthenticationMethod == AuthenticationType.Forms) + { + authBuilder.AddCookie(AuthenticationType.Forms.ToString(), options => + { + options.AccessDeniedPath = "/login?loginFailed=true"; + options.LoginPath = "/login"; + options.ExpireTimeSpan = TimeSpan.FromDays(7); + }); + } + else + { + authBuilder.AddNoAuthentication(); + } + + authBuilder.AddApiKey("API", options => + { + options.HeaderName = "X-Api-Key"; + options.QueryName = "apikey"; + options.ApiKey = config.ApiKey; + }); + + authBuilder.AddApiKey("SignalR", options => + { + options.HeaderName = "X-Api-Key"; + options.QueryName = "access_token"; + options.ApiKey = config.ApiKey; + }); + + return authBuilder; + } + } +} diff --git a/src/Lidarr.Http/Authentication/AuthenticationController.cs b/src/Lidarr.Http/Authentication/AuthenticationController.cs new file mode 100644 index 000000000..47d238b30 --- /dev/null +++ b/src/Lidarr.Http/Authentication/AuthenticationController.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Http.Authentication +{ + [AllowAnonymous] + [ApiController] + public class AuthenticationController : Controller + { + private readonly IAuthenticationService _authService; + private readonly IConfigFileProvider _configFileProvider; + + public AuthenticationController(IAuthenticationService authService, IConfigFileProvider configFileProvider) + { + _authService = authService; + _configFileProvider = configFileProvider; + } + + [HttpPost("login")] + public async Task Login([FromForm] LoginResource resource, [FromQuery] string returnUrl = null) + { + var user = _authService.Login(HttpContext.Request, resource.Username, resource.Password); + + if (user == null) + { + return Redirect($"~/login?returnUrl={returnUrl}&loginFailed=true"); + } + + var claims = new List + { + new Claim("user", user.Username), + new Claim("identifier", user.Identifier.ToString()), + new Claim("UiAuth", "true") + }; + + var authProperties = new AuthenticationProperties + { + IsPersistent = resource.RememberMe == "on" + }; + await HttpContext.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "identifier")), authProperties); + + return Redirect("/"); + } + + [HttpGet("logout")] + public async Task Logout() + { + _authService.Logout(HttpContext); + await HttpContext.SignOutAsync(); + return Redirect("/"); + } + } +} diff --git a/src/Lidarr.Http/Authentication/AuthenticationModule.cs b/src/Lidarr.Http/Authentication/AuthenticationModule.cs deleted file mode 100644 index 742b94e95..000000000 --- a/src/Lidarr.Http/Authentication/AuthenticationModule.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Nancy; -using Nancy.Authentication.Forms; -using Nancy.Extensions; -using Nancy.ModelBinding; -using NzbDrone.Core.Configuration; - -namespace Lidarr.Http.Authentication -{ - public class AuthenticationModule : NancyModule - { - private readonly IAuthenticationService _authService; - private readonly IConfigFileProvider _configFileProvider; - - public AuthenticationModule(IAuthenticationService authService, IConfigFileProvider configFileProvider) - { - _authService = authService; - _configFileProvider = configFileProvider; - Post("/login", x => Login(this.Bind())); - Get("/logout", x => Logout()); - } - - private Response Login(LoginResource resource) - { - var user = _authService.Login(Context, resource.Username, resource.Password); - - if (user == null) - { - var returnUrl = (string)Request.Query.returnUrl; - return Context.GetRedirect($"~/login?returnUrl={returnUrl}&loginFailed=true"); - } - - DateTime? expiry = null; - - if (resource.RememberMe) - { - expiry = DateTime.UtcNow.AddDays(7); - } - - return this.LoginAndRedirect(user.Identifier, expiry, _configFileProvider.UrlBase + "/"); - } - - private Response Logout() - { - _authService.Logout(Context); - - return this.LogoutAndRedirect(_configFileProvider.UrlBase + "/"); - } - } -} diff --git a/src/Lidarr.Http/Authentication/AuthenticationService.cs b/src/Lidarr.Http/Authentication/AuthenticationService.cs index 6ee799f2e..f1ff69f5b 100644 --- a/src/Lidarr.Http/Authentication/AuthenticationService.cs +++ b/src/Lidarr.Http/Authentication/AuthenticationService.cs @@ -1,28 +1,16 @@ -using System; -using System.Linq; -using System.Net; -using System.Security.Claims; -using System.Security.Principal; using Lidarr.Http.Extensions; -using Nancy; -using Nancy.Authentication.Basic; -using Nancy.Authentication.Forms; -using Nancy.Routing.Trie.Nodes; +using Microsoft.AspNetCore.Http; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; namespace Lidarr.Http.Authentication { - public interface IAuthenticationService : IUserValidator, IUserMapper + public interface IAuthenticationService { - void SetContext(NancyContext context); - - void LogUnauthorized(NancyContext context); - User Login(NancyContext context, string username, string password); - void Logout(NancyContext context); - bool IsAuthenticated(NancyContext context); + void LogUnauthorized(HttpRequest context); + User Login(HttpRequest request, string username, string password); + void Logout(HttpContext context); } public class AuthenticationService : IAuthenticationService @@ -34,9 +22,6 @@ namespace Lidarr.Http.Authentication private static string API_KEY; private static AuthenticationType AUTH_METHOD; - [ThreadStatic] - private static NancyContext _context; - public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService) { _userService = userService; @@ -44,13 +29,7 @@ namespace Lidarr.Http.Authentication AUTH_METHOD = configFileProvider.AuthenticationMethod; } - public void SetContext(NancyContext context) - { - // Validate and GetUserIdentifier don't have access to the NancyContext so get it from the pipeline earlier - _context = context; - } - - public User Login(NancyContext context, string username, string password) + public User Login(HttpRequest request, string username, string password) { if (AUTH_METHOD == AuthenticationType.None) { @@ -61,179 +40,50 @@ namespace Lidarr.Http.Authentication if (user != null) { - LogSuccess(context, username); + LogSuccess(request, username); return user; } - LogFailure(context, username); + LogFailure(request, username); return null; } - public void Logout(NancyContext context) + public void Logout(HttpContext context) { if (AUTH_METHOD == AuthenticationType.None) { return; } - if (context.CurrentUser != null) - { - LogLogout(context, context.CurrentUser.Identity.Name); - } - } - - public ClaimsPrincipal Validate(string username, string password) - { - if (AUTH_METHOD == AuthenticationType.None) - { - return new ClaimsPrincipal(new GenericIdentity(AnonymousUser)); - } - - var user = _userService.FindUser(username, password); - - if (user != null) - { - if (AUTH_METHOD != AuthenticationType.Basic) - { - // Don't log success for basic auth - LogSuccess(_context, username); - } - - return new ClaimsPrincipal(new GenericIdentity(user.Username)); - } - - LogFailure(_context, username); - - return null; - } - - public ClaimsPrincipal GetUserFromIdentifier(Guid identifier, NancyContext context) - { - if (AUTH_METHOD == AuthenticationType.None) - { - return new ClaimsPrincipal(new GenericIdentity(AnonymousUser)); - } - - var user = _userService.FindUser(identifier); - - if (user != null) - { - return new ClaimsPrincipal(new GenericIdentity(user.Username)); - } - - LogInvalidated(_context); - - return null; - } - - public bool IsAuthenticated(NancyContext context) - { - var apiKey = GetApiKey(context); - - if (context.Request.IsApiRequest()) - { - return ValidApiKey(apiKey); - } - - if (AUTH_METHOD == AuthenticationType.None) - { - return true; - } - - if (context.Request.IsFeedRequest()) - { - if (ValidUser(context) || ValidApiKey(apiKey)) - { - return true; - } - - return false; - } - - if (context.Request.IsLoginRequest()) - { - return true; - } - - if (context.Request.IsContentRequest()) - { - return true; - } - - if (context.Request.IsBundledJsRequest()) - { - return true; - } - - if (ValidUser(context)) - { - return true; - } - - return false; - } - - private bool ValidUser(NancyContext context) - { - if (context.CurrentUser != null) + if (context.User != null) { - return true; + LogLogout(context.Request, context.User.Identity.Name); } - - return false; - } - - private bool ValidApiKey(string apiKey) - { - if (API_KEY.Equals(apiKey)) - { - return true; - } - - return false; - } - - private string GetApiKey(NancyContext context) - { - var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault(); - var apiKeyQueryString = context.Request.Query["ApiKey"]; - - if (!apiKeyHeader.IsNullOrWhiteSpace()) - { - return apiKeyHeader; - } - - if (apiKeyQueryString.HasValue) - { - return apiKeyQueryString.Value; - } - - return context.Request.Headers.Authorization; } - public void LogUnauthorized(NancyContext context) + public void LogUnauthorized(HttpRequest context) { - _authLogger.Info("Auth-Unauthorized ip {0} url '{1}'", context.GetRemoteIP(), context.Request.Url.ToString()); + _authLogger.Info("Auth-Unauthorized ip {0} url '{1}'", context.GetRemoteIP(), context.Path); } - private void LogInvalidated(NancyContext context) + private void LogInvalidated(HttpRequest context) { _authLogger.Info("Auth-Invalidated ip {0}", context.GetRemoteIP()); } - private void LogFailure(NancyContext context, string username) + private void LogFailure(HttpRequest context, string username) { _authLogger.Warn("Auth-Failure ip {0} username '{1}'", context.GetRemoteIP(), username); } - private void LogSuccess(NancyContext context, string username) + private void LogSuccess(HttpRequest context, string username) { _authLogger.Info("Auth-Success ip {0} username '{1}'", context.GetRemoteIP(), username); } - private void LogLogout(NancyContext context, string username) + private void LogLogout(HttpRequest context, string username) { _authLogger.Info("Auth-Logout ip {0} username '{1}'", context.GetRemoteIP(), username); } diff --git a/src/Lidarr.Http/Authentication/BasicAuthenticationHandler.cs b/src/Lidarr.Http/Authentication/BasicAuthenticationHandler.cs new file mode 100644 index 000000000..7f198991e --- /dev/null +++ b/src/Lidarr.Http/Authentication/BasicAuthenticationHandler.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NzbDrone.Common.EnvironmentInfo; + +namespace Lidarr.Http.Authentication +{ + public class BasicAuthenticationHandler : AuthenticationHandler + { + private readonly IAuthenticationService _authService; + + public BasicAuthenticationHandler(IAuthenticationService authService, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + _authService = authService; + } + + protected override Task HandleAuthenticateAsync() + { + if (!Request.Headers.ContainsKey("Authorization")) + { + return Task.FromResult(AuthenticateResult.Fail("Authorization header missing.")); + } + + // Get authorization key + var authorizationHeader = Request.Headers["Authorization"].ToString(); + var authHeaderRegex = new Regex(@"Basic (.*)"); + + if (!authHeaderRegex.IsMatch(authorizationHeader)) + { + return Task.FromResult(AuthenticateResult.Fail("Authorization code not formatted properly.")); + } + + var authBase64 = Encoding.UTF8.GetString(Convert.FromBase64String(authHeaderRegex.Replace(authorizationHeader, "$1"))); + var authSplit = authBase64.Split(':', 2); + var authUsername = authSplit[0]; + var authPassword = authSplit.Length > 1 ? authSplit[1] : throw new Exception("Unable to get password"); + + var user = _authService.Login(Request, authUsername, authPassword); + + if (user == null) + { + return Task.FromResult(AuthenticateResult.Fail("The username or password is not correct.")); + } + + var claims = new List + { + new Claim("user", user.Username), + new Claim("identifier", user.Identifier.ToString()), + new Claim("UiAuth", "true") + }; + + var identity = new ClaimsIdentity(claims, "Basic", "user", "identifier"); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, "Basic"); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + Response.Headers.Add("WWW-Authenticate", $"Basic realm=\"{BuildInfo.AppName}\""); + Response.StatusCode = 401; + return Task.CompletedTask; + } + + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + { + Response.StatusCode = 403; + return Task.CompletedTask; + } + } +} diff --git a/src/Lidarr.Http/Authentication/EnableAuthInNancy.cs b/src/Lidarr.Http/Authentication/EnableAuthInNancy.cs deleted file mode 100644 index 9e37c7018..000000000 --- a/src/Lidarr.Http/Authentication/EnableAuthInNancy.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Text; -using Lidarr.Http.Extensions; -using Lidarr.Http.Extensions.Pipelines; -using Nancy; -using Nancy.Authentication.Basic; -using Nancy.Authentication.Forms; -using Nancy.Bootstrapper; -using Nancy.Cryptography; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Authentication; -using NzbDrone.Core.Configuration; - -namespace Lidarr.Http.Authentication -{ - public class EnableAuthInNancy : IRegisterNancyPipeline - { - private readonly IAuthenticationService _authenticationService; - private readonly IConfigService _configService; - private readonly IConfigFileProvider _configFileProvider; - private FormsAuthenticationConfiguration _formsAuthConfig; - - public EnableAuthInNancy(IAuthenticationService authenticationService, - IConfigService configService, - IConfigFileProvider configFileProvider) - { - _authenticationService = authenticationService; - _configService = configService; - _configFileProvider = configFileProvider; - } - - public int Order => 10; - - public void Register(IPipelines pipelines) - { - if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms) - { - RegisterFormsAuth(pipelines); - pipelines.AfterRequest.AddItemToEndOfPipeline(SlidingAuthenticationForFormsAuth); - } - else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic) - { - pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, BuildInfo.AppName)); - pipelines.BeforeRequest.AddItemToStartOfPipeline(CaptureContext); - } - - pipelines.BeforeRequest.AddItemToEndOfPipeline(RequiresAuthentication); - pipelines.AfterRequest.AddItemToEndOfPipeline(RemoveLoginHooksForApiCalls); - } - - private Response CaptureContext(NancyContext context) - { - _authenticationService.SetContext(context); - - return null; - } - - private Response RequiresAuthentication(NancyContext context) - { - Response response = null; - - if (!_authenticationService.IsAuthenticated(context)) - { - _authenticationService.LogUnauthorized(context); - response = new Response { StatusCode = HttpStatusCode.Unauthorized }; - } - - return response; - } - - private void RegisterFormsAuth(IPipelines pipelines) - { - FormsAuthentication.FormsAuthenticationCookieName = "LidarrAuth"; - - var cryptographyConfiguration = new CryptographyConfiguration( - new AesEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase, Encoding.ASCII.GetBytes(_configService.RijndaelSalt))), - new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt)))); - - _formsAuthConfig = new FormsAuthenticationConfiguration - { - RedirectUrl = _configFileProvider.UrlBase + "/login", - UserMapper = _authenticationService, - Path = GetCookiePath(), - CryptographyConfiguration = cryptographyConfiguration - }; - - FormsAuthentication.Enable(pipelines, _formsAuthConfig); - } - - private void RemoveLoginHooksForApiCalls(NancyContext context) - { - if (context.Request.IsApiRequest()) - { - if ((context.Response.StatusCode == HttpStatusCode.SeeOther && - context.Response.Headers["Location"].StartsWith($"{_configFileProvider.UrlBase}/login", StringComparison.InvariantCultureIgnoreCase)) || - context.Response.StatusCode == HttpStatusCode.Unauthorized) - { - context.Response = new { Error = "Unauthorized" }.AsResponse(context, HttpStatusCode.Unauthorized); - } - } - } - - private void SlidingAuthenticationForFormsAuth(NancyContext context) - { - if (context.CurrentUser == null) - { - return; - } - - var formsAuthCookieName = FormsAuthentication.FormsAuthenticationCookieName; - - if (!context.Request.Path.Equals("/logout") && - context.Request.Cookies.ContainsKey(formsAuthCookieName)) - { - var formsAuthCookieValue = context.Request.Cookies[formsAuthCookieName]; - - if (FormsAuthentication.DecryptAndValidateAuthenticationCookie(formsAuthCookieValue, _formsAuthConfig).IsNotNullOrWhiteSpace()) - { - var formsAuthCookie = new LidarrNancyCookie(formsAuthCookieName, formsAuthCookieValue, true, false, DateTime.UtcNow.AddDays(7)) - { - Path = GetCookiePath() - }; - - context.Response.WithCookie(formsAuthCookie); - } - } - } - - private string GetCookiePath() - { - var urlBase = _configFileProvider.UrlBase; - - if (urlBase.IsNullOrWhiteSpace()) - { - return "/"; - } - - return urlBase; - } - } -} diff --git a/src/Lidarr.Http/Authentication/LidarrNancyCookie.cs b/src/Lidarr.Http/Authentication/LidarrNancyCookie.cs deleted file mode 100644 index 1b701876b..000000000 --- a/src/Lidarr.Http/Authentication/LidarrNancyCookie.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using Nancy.Cookies; - -namespace Lidarr.Http.Authentication -{ - public class LidarrNancyCookie : NancyCookie - { - public LidarrNancyCookie(string name, string value) - : base(name, value) - { - } - - public LidarrNancyCookie(string name, string value, DateTime expires) - : base(name, value, expires) - { - } - - public LidarrNancyCookie(string name, string value, bool httpOnly) - : base(name, value, httpOnly) - { - } - - public LidarrNancyCookie(string name, string value, bool httpOnly, bool secure) - : base(name, value, httpOnly, secure) - { - } - - public LidarrNancyCookie(string name, string value, bool httpOnly, bool secure, DateTime? expires) - : base(name, value, httpOnly, secure, expires) - { - } - - public override string ToString() - { - return base.ToString() + "; SameSite=Lax"; - } - } -} diff --git a/src/Lidarr.Http/Authentication/LoginResource.cs b/src/Lidarr.Http/Authentication/LoginResource.cs index db1b94513..2cacd9c39 100644 --- a/src/Lidarr.Http/Authentication/LoginResource.cs +++ b/src/Lidarr.Http/Authentication/LoginResource.cs @@ -1,9 +1,9 @@ -namespace Lidarr.Http.Authentication +namespace Lidarr.Http.Authentication { public class LoginResource { public string Username { get; set; } public string Password { get; set; } - public bool RememberMe { get; set; } + public string RememberMe { get; set; } } } diff --git a/src/Lidarr.Http/Authentication/NoAuthenticationHandler.cs b/src/Lidarr.Http/Authentication/NoAuthenticationHandler.cs new file mode 100644 index 000000000..5152bc2b3 --- /dev/null +++ b/src/Lidarr.Http/Authentication/NoAuthenticationHandler.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Lidarr.Http.Authentication +{ + public class NoAuthenticationHandler : AuthenticationHandler + { + public NoAuthenticationHandler(IAuthenticationService authService, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + var claims = new List + { + new Claim("user", "Anonymous"), + new Claim("UiAuth", "true") + }; + + var identity = new ClaimsIdentity(claims, "NoAuth", "user", "identifier"); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, "NoAuth"); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } + } +} diff --git a/src/Lidarr.Http/Authentication/UiAuthorizationPolicyProvider.cs b/src/Lidarr.Http/Authentication/UiAuthorizationPolicyProvider.cs new file mode 100644 index 000000000..a5295a99f --- /dev/null +++ b/src/Lidarr.Http/Authentication/UiAuthorizationPolicyProvider.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Http.Authentication +{ + public class UiAuthorizationPolicyProvider : IAuthorizationPolicyProvider + { + private const string POLICY_NAME = "UI"; + private readonly IConfigFileProvider _config; + + public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; } + + public UiAuthorizationPolicyProvider(IOptions options, + IConfigFileProvider config) + { + FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options); + _config = config; + } + + public Task GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync(); + + public Task GetFallbackPolicyAsync() => FallbackPolicyProvider.GetFallbackPolicyAsync(); + + public Task GetPolicyAsync(string policyName) + { + if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase)) + { + var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString()) + .RequireAuthenticatedUser(); + return Task.FromResult(policy.Build()); + } + + return FallbackPolicyProvider.GetPolicyAsync(policyName); + } + } +} diff --git a/src/Lidarr.Http/ErrorManagement/ErrorHandler.cs b/src/Lidarr.Http/ErrorManagement/ErrorHandler.cs deleted file mode 100644 index ea371975d..000000000 --- a/src/Lidarr.Http/ErrorManagement/ErrorHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Lidarr.Http.Extensions; -using Nancy; -using Nancy.ErrorHandling; - -namespace Lidarr.Http.ErrorManagement -{ - public class ErrorHandler : IStatusCodeHandler - { - public bool HandlesStatusCode(HttpStatusCode statusCode, NancyContext context) - { - return true; - } - - public void Handle(HttpStatusCode statusCode, NancyContext context) - { - if (statusCode == HttpStatusCode.SeeOther || statusCode == HttpStatusCode.OK) - { - return; - } - - if (statusCode == HttpStatusCode.Continue) - { - context.Response = new Response { StatusCode = statusCode }; - return; - } - - if (statusCode == HttpStatusCode.Unauthorized) - { - return; - } - - if (context.Response.ContentType == "text/html" || context.Response.ContentType == "text/plain") - { - context.Response = new ErrorModel - { - Message = statusCode.ToString() - }.AsResponse(context, statusCode); - } - } - } -} diff --git a/src/Lidarr.Http/ErrorManagement/ErrorModel.cs b/src/Lidarr.Http/ErrorManagement/ErrorModel.cs index 04284f2a5..2de08910a 100644 --- a/src/Lidarr.Http/ErrorManagement/ErrorModel.cs +++ b/src/Lidarr.Http/ErrorManagement/ErrorModel.cs @@ -1,4 +1,8 @@ -using Lidarr.Http.Exceptions; +using System.Net; +using System.Threading.Tasks; +using Lidarr.Http.Exceptions; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.Serializer; namespace Lidarr.Http.ErrorManagement { @@ -17,5 +21,12 @@ namespace Lidarr.Http.ErrorManagement public ErrorModel() { } + + public Task WriteToResponse(HttpResponse response, HttpStatusCode statusCode = HttpStatusCode.InternalServerError) + { + response.StatusCode = (int)statusCode; + response.ContentType = "application/json"; + return STJson.SerializeAsync(this, response.Body); + } } } diff --git a/src/Lidarr.Http/ErrorManagement/LidarrErrorPipeline.cs b/src/Lidarr.Http/ErrorManagement/LidarrErrorPipeline.cs index abb06f842..8e2280117 100644 --- a/src/Lidarr.Http/ErrorManagement/LidarrErrorPipeline.cs +++ b/src/Lidarr.Http/ErrorManagement/LidarrErrorPipeline.cs @@ -1,13 +1,14 @@ -using System; using System.Data.SQLite; +using System.Net; +using System.Threading.Tasks; using FluentValidation; using Lidarr.Http.Exceptions; -using Lidarr.Http.Extensions; -using Nancy; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; using NLog; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Datastore; using NzbDrone.Core.Exceptions; -using HttpStatusCode = Nancy.HttpStatusCode; namespace Lidarr.Http.ErrorManagement { @@ -20,60 +21,81 @@ namespace Lidarr.Http.ErrorManagement _logger = logger; } - public Response HandleException(NancyContext context, Exception exception) + public async Task HandleException(HttpContext context) { _logger.Trace("Handling Exception"); + var response = context.Response; + var exceptionHandlerPathFeature = context.Features.Get(); + var exception = exceptionHandlerPathFeature?.Error; + + _logger.Warn(exception); + + var statusCode = HttpStatusCode.InternalServerError; + var errorModel = new ErrorModel + { + Message = exception.Message, + Description = exception.ToString() + }; + if (exception is ApiException apiException) { - _logger.Warn(apiException, "API Error"); - return apiException.ToErrorResponse(context); - } + _logger.Warn(apiException, "API Error:\n{0}", apiException.Message); + + /* var body = RequestStream.FromStream(context.Request.Body).AsString(); + _logger.Trace("Request body:\n{0}", body);*/ - if (exception is ValidationException validationException) + errorModel = new ErrorModel(apiException); + statusCode = apiException.StatusCode; + } + else if (exception is ValidationException validationException) { _logger.Warn("Invalid request {0}", validationException.Message); - return validationException.Errors.AsResponse(context, HttpStatusCode.BadRequest); + response.StatusCode = (int)HttpStatusCode.BadRequest; + response.ContentType = "application/json"; + await response.WriteAsync(STJson.ToJson(validationException.Errors)); + return; } - - if (exception is NzbDroneClientException clientException) + else if (exception is NzbDroneClientException clientException) { - return new ErrorModel + errorModel = new ErrorModel { Message = exception.Message, Description = exception.ToString() - }.AsResponse(context, (HttpStatusCode)clientException.StatusCode); + }; + statusCode = clientException.StatusCode; } - - if (exception is ModelNotFoundException notFoundException) + else if (exception is ModelNotFoundException notFoundException) { - return new ErrorModel + errorModel = new ErrorModel { Message = exception.Message, Description = exception.ToString() - }.AsResponse(context, HttpStatusCode.NotFound); + }; + statusCode = HttpStatusCode.NotFound; } - - if (exception is ModelConflictException conflictException) + else if (exception is ModelConflictException conflictException) { - return new ErrorModel + _logger.Error(exception, "DB error"); + errorModel = new ErrorModel { Message = exception.Message, Description = exception.ToString() - }.AsResponse(context, HttpStatusCode.Conflict); + }; + statusCode = HttpStatusCode.Conflict; } - - if (exception is SQLiteException sqLiteException) + else if (exception is SQLiteException sqLiteException) { if (context.Request.Method == "PUT" || context.Request.Method == "POST") { if (sqLiteException.Message.Contains("constraint failed")) { - return new ErrorModel + errorModel = new ErrorModel { Message = exception.Message, - }.AsResponse(context, HttpStatusCode.Conflict); + }; + statusCode = HttpStatusCode.Conflict; } } @@ -82,11 +104,7 @@ namespace Lidarr.Http.ErrorManagement _logger.Fatal(exception, "Request Failed. {0} {1}", context.Request.Method, context.Request.Path); - return new ErrorModel - { - Message = exception.Message, - Description = exception.ToString() - }.AsResponse(context, HttpStatusCode.InternalServerError); + await errorModel.WriteToResponse(response, statusCode); } } } diff --git a/src/Lidarr.Http/Exceptions/ApiException.cs b/src/Lidarr.Http/Exceptions/ApiException.cs index 9b7f56f95..391b4c9aa 100644 --- a/src/Lidarr.Http/Exceptions/ApiException.cs +++ b/src/Lidarr.Http/Exceptions/ApiException.cs @@ -1,8 +1,5 @@ using System; -using Lidarr.Http.ErrorManagement; -using Lidarr.Http.Extensions; -using Nancy; -using Nancy.Responses; +using System.Net; namespace Lidarr.Http.Exceptions { @@ -19,11 +16,6 @@ namespace Lidarr.Http.Exceptions Content = content; } - public JsonResponse ToErrorResponse(NancyContext context) - { - return new ErrorModel(this).AsResponse(context, StatusCode); - } - private static string GetMessage(HttpStatusCode statusCode, object content) { var result = statusCode.ToString(); diff --git a/src/Lidarr.Http/Extensions/NancyJsonSerializer.cs b/src/Lidarr.Http/Extensions/NancyJsonSerializer.cs deleted file mode 100644 index e0ad0e3f6..000000000 --- a/src/Lidarr.Http/Extensions/NancyJsonSerializer.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.Json; -using Nancy; -using Nancy.Responses.Negotiation; -using NzbDrone.Common.Serializer; - -namespace Lidarr.Http.Extensions -{ - public class NancyJsonSerializer : ISerializer - { - protected readonly JsonSerializerOptions _serializerSettings; - - public NancyJsonSerializer() - { - _serializerSettings = STJson.GetSerializerSettings(); - } - - public bool CanSerialize(MediaRange contentType) - { - return contentType == "application/json"; - } - - public void Serialize(MediaRange contentType, TModel model, Stream outputStream) - { - STJson.Serialize(model, outputStream, _serializerSettings); - } - - public IEnumerable Extensions { get; private set; } - } -} diff --git a/src/Lidarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs deleted file mode 100644 index 2a75cbcd1..000000000 --- a/src/Lidarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Lidarr.Http.Frontend; -using Nancy; -using Nancy.Bootstrapper; - -namespace Lidarr.Http.Extensions.Pipelines -{ - public class CacheHeaderPipeline : IRegisterNancyPipeline - { - private readonly ICacheableSpecification _cacheableSpecification; - - public CacheHeaderPipeline(ICacheableSpecification cacheableSpecification) - { - _cacheableSpecification = cacheableSpecification; - } - - public int Order => 0; - - public void Register(IPipelines pipelines) - { - pipelines.AfterRequest.AddItemToStartOfPipeline(Handle); - } - - private void Handle(NancyContext context) - { - if (context.Request.Method == "OPTIONS") - { - return; - } - - if (_cacheableSpecification.IsCacheable(context)) - { - context.Response.Headers.EnableCache(); - } - else - { - context.Response.Headers.DisableCache(); - } - } - } -} diff --git a/src/Lidarr.Http/Extensions/Pipelines/CorsPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/CorsPipeline.cs deleted file mode 100644 index 10d47e738..000000000 --- a/src/Lidarr.Http/Extensions/Pipelines/CorsPipeline.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Linq; -using Nancy; -using Nancy.Bootstrapper; -using NzbDrone.Common.Extensions; - -namespace Lidarr.Http.Extensions.Pipelines -{ - public class CorsPipeline : IRegisterNancyPipeline - { - public int Order => 0; - - public void Register(IPipelines pipelines) - { - pipelines.BeforeRequest.AddItemToEndOfPipeline(HandleRequest); - pipelines.AfterRequest.AddItemToEndOfPipeline(HandleResponse); - } - - private Response HandleRequest(NancyContext context) - { - if (context == null || context.Request.Method != "OPTIONS") - { - return null; - } - - var response = new Response() - .WithStatusCode(HttpStatusCode.OK) - .WithContentType(""); - ApplyResponseHeaders(response, context.Request); - return response; - } - - private void HandleResponse(NancyContext context) - { - if (context == null || context.Response.Headers.ContainsKey(AccessControlHeaders.AllowOrigin)) - { - return; - } - - ApplyResponseHeaders(context.Response, context.Request); - } - - private static void ApplyResponseHeaders(Response response, Request request) - { - if (request.IsApiRequest()) - { - // Allow Cross-Origin access to the API since it's protected with the apikey, and nothing else. - ApplyCorsResponseHeaders(response, request, "*", "GET, OPTIONS, PATCH, POST, PUT, DELETE"); - } - else if (request.IsSharedContentRequest()) - { - // Allow Cross-Origin access to specific shared content such as mediacovers and images. - ApplyCorsResponseHeaders(response, request, "*", "GET, OPTIONS"); - } - - // Disallow Cross-Origin access for any other route. - } - - private static void ApplyCorsResponseHeaders(Response response, Request request, string allowOrigin, string allowedMethods) - { - response.Headers.Add(AccessControlHeaders.AllowOrigin, allowOrigin); - - if (request.Method == "OPTIONS") - { - if (response.Headers.ContainsKey("Allow")) - { - allowedMethods = response.Headers["Allow"]; - } - - response.Headers.Add(AccessControlHeaders.AllowMethods, allowedMethods); - - if (request.Headers[AccessControlHeaders.RequestHeaders].Any()) - { - var requestedHeaders = request.Headers[AccessControlHeaders.RequestHeaders].Join(", "); - - response.Headers.Add(AccessControlHeaders.AllowHeaders, requestedHeaders); - } - } - } - } -} diff --git a/src/Lidarr.Http/Extensions/Pipelines/GZipPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/GZipPipeline.cs deleted file mode 100644 index 329766559..000000000 --- a/src/Lidarr.Http/Extensions/Pipelines/GZipPipeline.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Linq; -using Nancy; -using Nancy.Bootstrapper; -using NLog; -using NzbDrone.Common.EnvironmentInfo; - -namespace Lidarr.Http.Extensions.Pipelines -{ - public class GzipCompressionPipeline : IRegisterNancyPipeline - { - private readonly Logger _logger; - - public int Order => 0; - - private readonly Action, Stream> _writeGZipStream; - - public GzipCompressionPipeline(Logger logger) - { - _logger = logger; - - _writeGZipStream = (Action, Stream>)WriteGZipStream; - } - - public void Register(IPipelines pipelines) - { - pipelines.AfterRequest.AddItemToEndOfPipeline(CompressResponse); - } - - private void CompressResponse(NancyContext context) - { - var request = context.Request; - var response = context.Response; - - try - { - if ( - response.Contents != Response.NoBody - && !response.ContentType.Contains("image") - && !response.ContentType.Contains("font") - && request.Headers.AcceptEncoding.Any(x => x.Contains("gzip")) - && !AlreadyGzipEncoded(response) - && !ContentLengthIsTooSmall(response)) - { - var contents = response.Contents; - - response.Headers["Content-Encoding"] = "gzip"; - response.Contents = responseStream => _writeGZipStream(contents, responseStream); - } - } - catch (Exception ex) - { - _logger.Error(ex, "Unable to gzip response"); - throw; - } - } - - private static void WriteGZipStreamMono(Action innerContent, Stream targetStream) - { - using (var membuffer = new MemoryStream()) - { - WriteGZipStream(innerContent, membuffer); - membuffer.Position = 0; - membuffer.CopyTo(targetStream); - } - } - - private static void WriteGZipStream(Action innerContent, Stream targetStream) - { - using (var gzip = new GZipStream(targetStream, CompressionMode.Compress, true)) - using (var buffered = new BufferedStream(gzip, 8192)) - { - innerContent.Invoke(buffered); - } - } - - private static bool ContentLengthIsTooSmall(Response response) - { - var contentLength = response.Headers.TryGetValue("Content-Length", out var value) ? value : null; - - if (contentLength != null && long.Parse(contentLength) < 1024) - { - return true; - } - - return false; - } - - private static bool AlreadyGzipEncoded(Response response) - { - var contentEncoding = response.Headers.TryGetValue("Content-Encoding", out var value) ? value : null; - - if (contentEncoding == "gzip") - { - return true; - } - - return false; - } - } -} diff --git a/src/Lidarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs deleted file mode 100644 index 4177699a3..000000000 --- a/src/Lidarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Nancy.Bootstrapper; - -namespace Lidarr.Http.Extensions.Pipelines -{ - public interface IRegisterNancyPipeline - { - int Order { get; } - - void Register(IPipelines pipelines); - } -} diff --git a/src/Lidarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs deleted file mode 100644 index 60fa94bcb..000000000 --- a/src/Lidarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Lidarr.Http.Frontend; -using Nancy; -using Nancy.Bootstrapper; - -namespace Lidarr.Http.Extensions.Pipelines -{ - public class IfModifiedPipeline : IRegisterNancyPipeline - { - private readonly ICacheableSpecification _cacheableSpecification; - - public IfModifiedPipeline(ICacheableSpecification cacheableSpecification) - { - _cacheableSpecification = cacheableSpecification; - } - - public int Order => 0; - - public void Register(IPipelines pipelines) - { - pipelines.BeforeRequest.AddItemToStartOfPipeline(Handle); - } - - private Response Handle(NancyContext context) - { - if (_cacheableSpecification.IsCacheable(context) && context.Request.Headers.IfModifiedSince.HasValue) - { - var response = new Response { ContentType = MimeTypes.GetMimeType(context.Request.Path), StatusCode = HttpStatusCode.NotModified }; - response.Headers.EnableCache(); - return response; - } - - return null; - } - } -} diff --git a/src/Lidarr.Http/Extensions/Pipelines/LidarrVersionPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/LidarrVersionPipeline.cs deleted file mode 100644 index 289f5a74d..000000000 --- a/src/Lidarr.Http/Extensions/Pipelines/LidarrVersionPipeline.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Nancy; -using Nancy.Bootstrapper; -using NzbDrone.Common.EnvironmentInfo; - -namespace Lidarr.Http.Extensions.Pipelines -{ - public class LidarrVersionPipeline : IRegisterNancyPipeline - { - public int Order => 0; - - public void Register(IPipelines pipelines) - { - pipelines.AfterRequest.AddItemToStartOfPipeline(Handle); - } - - private void Handle(NancyContext context) - { - if (!context.Response.Headers.ContainsKey("X-Application-Version")) - { - context.Response.Headers.Add("X-Application-Version", BuildInfo.Version.ToString()); - } - } - } -} diff --git a/src/Lidarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs deleted file mode 100644 index ff8e66c11..000000000 --- a/src/Lidarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Threading; -using Lidarr.Http.ErrorManagement; -using Lidarr.Http.Extensions; -using Lidarr.Http.Extensions.Pipelines; -using Nancy; -using Nancy.Bootstrapper; -using NLog; -using NzbDrone.Common.Extensions; - -namespace NzbDrone.Api.Extensions.Pipelines -{ - public class RequestLoggingPipeline : IRegisterNancyPipeline - { - private static readonly Logger _loggerHttp = LogManager.GetLogger("Http"); - private static readonly Logger _loggerApi = LogManager.GetLogger("Api"); - - private static int _requestSequenceID; - - private readonly LidarrErrorPipeline _errorPipeline; - - public RequestLoggingPipeline(LidarrErrorPipeline errorPipeline) - { - _errorPipeline = errorPipeline; - } - - public int Order => 100; - - public void Register(IPipelines pipelines) - { - pipelines.BeforeRequest.AddItemToStartOfPipeline(LogStart); - pipelines.AfterRequest.AddItemToEndOfPipeline(LogEnd); - pipelines.OnError.AddItemToEndOfPipeline(LogError); - } - - private Response LogStart(NancyContext context) - { - var id = Interlocked.Increment(ref _requestSequenceID); - - context.Items["ApiRequestSequenceID"] = id; - context.Items["ApiRequestStartTime"] = DateTime.UtcNow; - - var reqPath = GetRequestPathAndQuery(context.Request); - - _loggerHttp.Trace("Req: {0} [{1}] {2} (from {3})", id, context.Request.Method, reqPath, GetOrigin(context)); - - return null; - } - - private void LogEnd(NancyContext context) - { - var id = (int)context.Items["ApiRequestSequenceID"]; - var startTime = (DateTime)context.Items["ApiRequestStartTime"]; - - var endTime = DateTime.UtcNow; - var duration = endTime - startTime; - - var reqPath = GetRequestPathAndQuery(context.Request); - - _loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); - - if (context.Request.IsApiRequest()) - { - _loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); - } - } - - private Response LogError(NancyContext context, Exception exception) - { - var response = _errorPipeline.HandleException(context, exception); - - context.Response = response; - - LogEnd(context); - - context.Response = null; - - return response; - } - - private static string GetRequestPathAndQuery(Request request) - { - if (request.Url.Query.IsNotNullOrWhiteSpace()) - { - return string.Concat(request.Url.Path, request.Url.Query); - } - else - { - return request.Url.Path; - } - } - - private static string GetOrigin(NancyContext context) - { - if (context.Request.Headers.UserAgent.IsNullOrWhiteSpace()) - { - return context.GetRemoteIP(); - } - else - { - return $"{context.GetRemoteIP()} {context.Request.Headers.UserAgent}"; - } - } - } -} diff --git a/src/Lidarr.Http/Extensions/Pipelines/SetCookieHeaderPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/SetCookieHeaderPipeline.cs deleted file mode 100644 index f0ed8f76b..000000000 --- a/src/Lidarr.Http/Extensions/Pipelines/SetCookieHeaderPipeline.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Linq; -using Nancy; -using Nancy.Bootstrapper; - -namespace Lidarr.Http.Extensions.Pipelines -{ - public class SetCookieHeaderPipeline : IRegisterNancyPipeline - { - public int Order => 99; - - public void Register(IPipelines pipelines) - { - pipelines.AfterRequest.AddItemToEndOfPipeline((Action)Handle); - } - - private void Handle(NancyContext context) - { - if (context.Request.IsContentRequest() || context.Request.IsBundledJsRequest()) - { - var authCookie = context.Response.Cookies.FirstOrDefault(c => c.Name == "SonarrAuth"); - - if (authCookie != null) - { - context.Response.Cookies.Remove(authCookie); - } - } - } - } -} diff --git a/src/Lidarr.Http/Extensions/Pipelines/UrlBasePipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/UrlBasePipeline.cs deleted file mode 100644 index 1b512e2eb..000000000 --- a/src/Lidarr.Http/Extensions/Pipelines/UrlBasePipeline.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Nancy; -using Nancy.Bootstrapper; -using Nancy.Responses; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; - -namespace Lidarr.Http.Extensions.Pipelines -{ - public class UrlBasePipeline : IRegisterNancyPipeline - { - private readonly string _urlBase; - - public UrlBasePipeline(IConfigFileProvider configFileProvider) - { - _urlBase = configFileProvider.UrlBase; - } - - public int Order => 99; - - public void Register(IPipelines pipelines) - { - if (_urlBase.IsNotNullOrWhiteSpace()) - { - pipelines.BeforeRequest.AddItemToStartOfPipeline(Handle); - } - } - - private Response Handle(NancyContext context) - { - var basePath = context.Request.Url.BasePath; - - if (basePath.IsNullOrWhiteSpace()) - { - return new RedirectResponse($"{_urlBase}{context.Request.Path}{context.Request.Url.Query}"); - } - - if (_urlBase != basePath) - { - return new NotFoundResponse(); - } - - return null; - } - } -} diff --git a/src/Lidarr.Http/Extensions/ReqResExtensions.cs b/src/Lidarr.Http/Extensions/ReqResExtensions.cs deleted file mode 100644 index 541f305fc..000000000 --- a/src/Lidarr.Http/Extensions/ReqResExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Nancy; -using Nancy.Responses; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Serializer; - -namespace Lidarr.Http.Extensions -{ - public static class ReqResExtensions - { - private static readonly NancyJsonSerializer NancySerializer = new NancyJsonSerializer(); - private static readonly string Expires = DateTime.UtcNow.AddYears(1).ToString("r"); - - public static readonly string LastModified = BuildInfo.BuildDateTime.ToString("r"); - - public static T FromJson(this Stream body) - where T : class, new() - { - return FromJson(body, typeof(T)); - } - - public static T FromJson(this Stream body, Type type) - { - return (T)FromJson(body, type); - } - - public static object FromJson(this Stream body, Type type) - { - body.Position = 0; - return STJson.Deserialize(body, type); - } - - public static JsonResponse AsResponse(this TModel model, NancyContext context, HttpStatusCode statusCode = HttpStatusCode.OK) - { - var response = new JsonResponse(model, NancySerializer, context.Environment) { StatusCode = statusCode }; - response.Headers.DisableCache(); - - return response; - } - - public static IDictionary DisableCache(this IDictionary headers) - { - headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0"; - headers["Pragma"] = "no-cache"; - headers["Expires"] = "0"; - - return headers; - } - - public static IDictionary EnableCache(this IDictionary headers) - { - headers["Cache-Control"] = "max-age=31536000, public"; - headers["Expires"] = Expires; - headers["Last-Modified"] = LastModified; - headers["Age"] = "193266"; - - return headers; - } - } -} diff --git a/src/Lidarr.Http/Extensions/RequestExtensions.cs b/src/Lidarr.Http/Extensions/RequestExtensions.cs index 11f944c80..59c6aef80 100644 --- a/src/Lidarr.Http/Extensions/RequestExtensions.cs +++ b/src/Lidarr.Http/Extensions/RequestExtensions.cs @@ -1,124 +1,173 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net; -using Nancy; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Exceptions; namespace Lidarr.Http.Extensions { public static class RequestExtensions { - public static bool IsApiRequest(this Request request) + // See src/Lidarr.Api.V1/Queue/QueueModule.cs + private static readonly HashSet VALID_SORT_KEYS = new HashSet(StringComparer.OrdinalIgnoreCase) { - return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsFeedRequest(this Request request) - { - return request.Path.StartsWith("/feed/", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsSignalRRequest(this Request request) - { - return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsLocalRequest(this Request request) + "authors.sortname", //Workaround authors table properties not being added on isValidSortKey call + "timeleft", + "estimatedCompletionTime", + "protocol", + "indexer", + "downloadClient", + "quality", + "status", + "title", + "progress" + }; + + private static readonly HashSet EXCLUDED_KEYS = new HashSet(StringComparer.InvariantCultureIgnoreCase) { - return request.UserHostAddress.Equals("localhost") || - request.UserHostAddress.Equals("127.0.0.1") || - request.UserHostAddress.Equals("::1"); - } - - public static bool IsLoginRequest(this Request request) + "page", + "pageSize", + "sortKey", + "sortDirection", + "filterKey", + "filterValue", + }; + + public static bool IsApiRequest(this HttpRequest request) { - return request.Path.Equals("/login", StringComparison.InvariantCultureIgnoreCase); + return request.Path.StartsWithSegments("/api", StringComparison.InvariantCultureIgnoreCase); } - public static bool IsContentRequest(this Request request) - { - return request.Path.StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool GetBooleanQueryParameter(this Request request, string parameter, bool defaultValue = false) + public static bool GetBooleanQueryParameter(this HttpRequest request, string parameter, bool defaultValue = false) { var parameterValue = request.Query[parameter]; - if (parameterValue.HasValue) + if (parameterValue.Any()) { - return bool.Parse(parameterValue.Value); + return bool.Parse(parameterValue.ToString()); } return defaultValue; } - public static bool IsBundledJsRequest(this Request request) + public static PagingResource ReadPagingResourceFromRequest(this HttpRequest request) { - return !request.Path.EqualsIgnoreCase("/initialize.js") && request.Path.EndsWith(".js", StringComparison.InvariantCultureIgnoreCase); - } + if (!int.TryParse(request.Query["PageSize"].ToString(), out var pageSize)) + { + pageSize = 10; + } - public static bool IsSharedContentRequest(this Request request) - { - return request.Path.StartsWith("/MediaCover/", StringComparison.InvariantCultureIgnoreCase) || - request.Path.StartsWith("/Content/Images/", StringComparison.InvariantCultureIgnoreCase); - } + if (!int.TryParse(request.Query["Page"].ToString(), out var page)) + { + page = 1; + } - public static int GetIntegerQueryParameter(this Request request, string parameter, int defaultValue = 0) - { - var parameterValue = request.Query[parameter]; + var pagingResource = new PagingResource + { + PageSize = pageSize, + Page = page, + Filters = new List() + }; - if (parameterValue.HasValue) + if (request.Query["SortKey"].Any()) { - return int.Parse(parameterValue.Value); + var sortKey = request.Query["SortKey"].ToString(); + + if (!VALID_SORT_KEYS.Contains(sortKey) && + !TableMapping.Mapper.IsValidSortKey(sortKey)) + { + throw new BadRequestException($"Invalid sort key {sortKey}"); + } + + pagingResource.SortKey = sortKey; + + if (request.Query["SortDirection"].Any()) + { + pagingResource.SortDirection = request.Query["SortDirection"].ToString() + .Equals("ascending", StringComparison.InvariantCultureIgnoreCase) + ? SortDirection.Ascending + : SortDirection.Descending; + } } - return defaultValue; - } + // For backwards compatibility with v2 + if (request.Query["FilterKey"].Any()) + { + var filter = new PagingResourceFilter + { + Key = request.Query["FilterKey"].ToString() + }; - public static Guid GetGuidQueryParameter(this Request request, string parameter, Guid defaultValue = default) - { - var parameterValue = request.Query[parameter]; + if (request.Query["FilterValue"].Any()) + { + filter.Value = request.Query["FilterValue"].ToString(); + } - if (parameterValue.HasValue) + pagingResource.Filters.Add(filter); + } + + // v3 uses filters in key=value format + foreach (var pair in request.Query) { - return Guid.Parse(parameterValue.Value); + if (EXCLUDED_KEYS.Contains(pair.Key)) + { + continue; + } + + pagingResource.Filters.Add(new PagingResourceFilter + { + Key = pair.Key, + Value = pair.Value.ToString() + }); } - return defaultValue; + return pagingResource; } - public static int? GetNullableIntegerQueryParameter(this Request request, string parameter, int? defaultValue = null) + public static PagingResource ApplyToPage(this PagingSpec pagingSpec, Func, PagingSpec> function, Converter mapper) { - var parameterValue = request.Query[parameter]; + pagingSpec = function(pagingSpec); - if (parameterValue.HasValue) + return new PagingResource { - return int.Parse(parameterValue.Value); - } + Page = pagingSpec.Page, + PageSize = pagingSpec.PageSize, + SortDirection = pagingSpec.SortDirection, + SortKey = pagingSpec.SortKey, + TotalRecords = pagingSpec.TotalRecords, + Records = pagingSpec.Records.ConvertAll(mapper) + }; + } - return defaultValue; + public static string GetRemoteIP(this HttpContext context) + { + return context?.Request?.GetRemoteIP() ?? "Unknown"; } - public static string GetRemoteIP(this NancyContext context) + public static string GetRemoteIP(this HttpRequest request) { - if (context == null || context.Request == null) + if (request == null) { return "Unknown"; } - var remoteAddress = context.Request.UserHostAddress; - IPAddress remoteIP; + var remoteIP = request.HttpContext.Connection.RemoteIpAddress; + var remoteAddress = remoteIP.ToString(); // Only check if forwarded by a local network reverse proxy - if (IPAddress.TryParse(remoteAddress, out remoteIP) && remoteIP.IsLocalAddress()) + if (remoteIP.IsLocalAddress()) { - var realIPHeader = context.Request.Headers["X-Real-IP"]; + var realIPHeader = request.Headers["X-Real-IP"]; if (realIPHeader.Any()) { return realIPHeader.First().ToString(); } - var forwardedForHeader = context.Request.Headers["X-Forwarded-For"]; + var forwardedForHeader = request.Headers["X-Forwarded-For"]; if (forwardedForHeader.Any()) { // Get the first address that was forwarded by a local IP to prevent remote clients faking another proxy @@ -141,5 +190,18 @@ namespace Lidarr.Http.Extensions return remoteAddress; } + + public static void DisableCache(this IHeaderDictionary headers) + { + headers["Cache-Control"] = "no-cache, no-store"; + headers["Expires"] = "-1"; + headers["Pragma"] = "no-cache"; + } + + public static void EnableCache(this IHeaderDictionary headers) + { + headers["Cache-Control"] = "max-age=31536000, public"; + headers["Last-Modified"] = BuildInfo.BuildDateTime.ToString("r"); + } } } diff --git a/src/Lidarr.Http/Frontend/CacheableSpecification.cs b/src/Lidarr.Http/Frontend/CacheableSpecification.cs deleted file mode 100644 index b5a4d74df..000000000 --- a/src/Lidarr.Http/Frontend/CacheableSpecification.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using Nancy; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Extensions; - -namespace Lidarr.Http.Frontend -{ - public interface ICacheableSpecification - { - bool IsCacheable(NancyContext context); - } - - public class CacheableSpecification : ICacheableSpecification - { - public bool IsCacheable(NancyContext context) - { - if (!RuntimeInfo.IsProduction) - { - return false; - } - - if (((DynamicDictionary)context.Request.Query).ContainsKey("h")) - { - return true; - } - - if (context.Request.Path.StartsWith("/api", StringComparison.CurrentCultureIgnoreCase)) - { - if (context.Request.Path.ContainsIgnoreCase("/MediaCover")) - { - return true; - } - - return false; - } - - if (context.Request.Path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase)) - { - return false; - } - - if (context.Request.Path.EndsWith("index.js")) - { - return false; - } - - if (context.Request.Path.EndsWith("initialize.js")) - { - return false; - } - - if (context.Request.Path.StartsWith("/feed", StringComparison.CurrentCultureIgnoreCase)) - { - return false; - } - - if (context.Request.Path.StartsWith("/log", StringComparison.CurrentCultureIgnoreCase) && - context.Request.Path.EndsWith(".txt", StringComparison.CurrentCultureIgnoreCase)) - { - return false; - } - - if (context.Response != null) - { - if (context.Response.ContentType.Contains("text/html")) - { - return false; - } - } - - return true; - } - } -} diff --git a/src/Lidarr.Http/Frontend/InitializeJsModule.cs b/src/Lidarr.Http/Frontend/InitializeJsController.cs similarity index 72% rename from src/Lidarr.Http/Frontend/InitializeJsModule.cs rename to src/Lidarr.Http/Frontend/InitializeJsController.cs index 4231a20fe..ef7fd5112 100644 --- a/src/Lidarr.Http/Frontend/InitializeJsModule.cs +++ b/src/Lidarr.Http/Frontend/InitializeJsController.cs @@ -1,7 +1,6 @@ -using System.IO; using System.Text; -using Nancy; -using Nancy.Responses; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Analytics; @@ -9,7 +8,9 @@ using NzbDrone.Core.Configuration; namespace Lidarr.Http.Frontend { - public class InitializeJsModule : NancyModule + [Authorize(Policy = "UI")] + [ApiController] + public class InitializeJsController : Controller { private readonly IConfigFileProvider _configFileProvider; private readonly IAnalyticsService _analyticsService; @@ -18,36 +19,21 @@ namespace Lidarr.Http.Frontend private static string _urlBase; private string _generatedContent; - public InitializeJsModule(IConfigFileProvider configFileProvider, - IAnalyticsService analyticsService) + public InitializeJsController(IConfigFileProvider configFileProvider, + IAnalyticsService analyticsService) { _configFileProvider = configFileProvider; _analyticsService = analyticsService; _apiKey = configFileProvider.ApiKey; _urlBase = configFileProvider.UrlBase; - - Get("/initialize.js", x => Index()); } - private Response Index() + [HttpGet("/initialize.js")] + public IActionResult Index() { // TODO: Move away from window.Lidarr and prefetch the information returned here when starting the UI - return new StreamResponse(GetContentStream, "application/javascript"); - } - - private Stream GetContentStream() - { - var text = GetContent(); - - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - - writer.Write(text); - writer.Flush(); - stream.Position = 0; - - return stream; + return Content(GetContent(), "application/javascript"); } private string GetContent() diff --git a/src/Lidarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Lidarr.Http/Frontend/Mappers/HtmlMapperBase.cs index 8bb7e70d5..f56e1c79a 100644 --- a/src/Lidarr.Http/Frontend/Mappers/HtmlMapperBase.cs +++ b/src/Lidarr.Http/Frontend/Mappers/HtmlMapperBase.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Text.RegularExpressions; -using Nancy; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -40,13 +39,14 @@ namespace Lidarr.Http.Frontend.Mappers return stream; } - public override Response GetResponse(string resourceUrl) + /* + public override IActionResult GetResponse(string resourceUrl) { var response = base.GetResponse(resourceUrl); response.Headers["X-UA-Compatible"] = "IE=edge"; return response; - } + }*/ protected string GetHtmlText() { diff --git a/src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs b/src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs index a66984d79..0edecbd4c 100644 --- a/src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs +++ b/src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs @@ -1,4 +1,4 @@ -using Nancy; +using Microsoft.AspNetCore.Mvc; namespace Lidarr.Http.Frontend.Mappers { @@ -6,6 +6,6 @@ namespace Lidarr.Http.Frontend.Mappers { string Map(string resourceUrl); bool CanHandle(string resourceUrl); - Response GetResponse(string resourceUrl); + IActionResult GetResponse(string resourceUrl); } } diff --git a/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index f6216a415..949c9a2d3 100644 --- a/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs @@ -1,7 +1,7 @@ using System; using System.IO; -using Nancy; -using Nancy.Responses; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -13,14 +13,14 @@ namespace Lidarr.Http.Frontend.Mappers private readonly IDiskProvider _diskProvider; private readonly Logger _logger; private readonly StringComparison _caseSensitive; - - private static readonly NotFoundResponse NotFoundResponse = new NotFoundResponse(); + private readonly IContentTypeProvider _mimeTypeProvider; protected StaticResourceMapperBase(IDiskProvider diskProvider, Logger logger) { _diskProvider = diskProvider; _logger = logger; + _mimeTypeProvider = new FileExtensionContentTypeProvider(); _caseSensitive = RuntimeInfo.IsProduction ? DiskProviderBase.PathStringComparison : StringComparison.OrdinalIgnoreCase; } @@ -28,19 +28,23 @@ namespace Lidarr.Http.Frontend.Mappers public abstract bool CanHandle(string resourceUrl); - public virtual Response GetResponse(string resourceUrl) + public virtual IActionResult GetResponse(string resourceUrl) { var filePath = Map(resourceUrl); if (_diskProvider.FileExists(filePath, _caseSensitive)) { - var response = new StreamResponse(() => GetContentStream(filePath), MimeTypes.GetMimeType(filePath)); - return new MaterialisingResponse(response); + if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType)) + { + contentType = "application/octet-stream"; + } + + return new FileStreamResult(GetContentStream(filePath), contentType); } _logger.Warn("File {0} not found", filePath); - return NotFoundResponse; + return null; } protected virtual Stream GetContentStream(string filePath) diff --git a/src/Lidarr.Http/Frontend/StaticResourceController.cs b/src/Lidarr.Http/Frontend/StaticResourceController.cs new file mode 100644 index 000000000..5709f7e06 --- /dev/null +++ b/src/Lidarr.Http/Frontend/StaticResourceController.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Lidarr.Http.Frontend.Mappers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using NLog; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Http.Frontend +{ + [Authorize(Policy="UI")] + [ApiController] + public class StaticResourceController : Controller + { + private readonly string _urlBase; + private readonly string _loginPath; + private readonly IEnumerable _requestMappers; + private readonly Logger _logger; + + public StaticResourceController(IConfigFileProvider configFileProvider, + IAppFolderInfo appFolderInfo, + IEnumerable requestMappers, + Logger logger) + { + _urlBase = configFileProvider.UrlBase.Trim('/'); + _requestMappers = requestMappers; + _logger = logger; + + _loginPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html"); + } + + [AllowAnonymous] + [HttpGet("login")] + public IActionResult LoginPage() + { + return PhysicalFile(_loginPath, "text/html"); + } + + [EnableCors("AllowGet")] + [AllowAnonymous] + [HttpGet("/content/{**path:regex(^(?!api/).*)}")] + public IActionResult IndexContent([FromRoute] string path) + { + return MapResource("Content/" + path); + } + + [HttpGet("")] + [HttpGet("/{**path:regex(^(?!api/).*)}")] + public IActionResult Index([FromRoute] string path) + { + return MapResource(path); + } + + private IActionResult MapResource(string path) + { + path = "/" + (path ?? ""); + + var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path)); + + if (mapper != null) + { + return mapper.GetResponse(path) ?? NotFound(); + } + + _logger.Warn("Couldn't find handler for {0}", path); + + return NotFound(); + } + } +} diff --git a/src/Lidarr.Http/Frontend/StaticResourceModule.cs b/src/Lidarr.Http/Frontend/StaticResourceModule.cs deleted file mode 100644 index e32c47b3d..000000000 --- a/src/Lidarr.Http/Frontend/StaticResourceModule.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Lidarr.Http.Frontend.Mappers; -using Nancy; -using NLog; - -namespace Lidarr.Http.Frontend -{ - public class StaticResourceModule : NancyModule - { - private readonly IEnumerable _requestMappers; - private readonly Logger _logger; - - public StaticResourceModule(IEnumerable requestMappers, Logger logger) - { - _requestMappers = requestMappers; - _logger = logger; - - Get("/{resource*}", x => Index()); - Get("/", x => Index()); - } - - private Response Index() - { - var path = Request.Url.Path; - - if ( - string.IsNullOrWhiteSpace(path) || - path.StartsWith("/api", StringComparison.CurrentCultureIgnoreCase) || - path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase)) - { - return new NotFoundResponse(); - } - - var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path)); - - if (mapper != null) - { - return mapper.GetResponse(path); - } - - _logger.Warn("Couldn't find handler for {0}", path); - - return new NotFoundResponse(); - } - } -} diff --git a/src/Lidarr.Http/Lidarr.Http.csproj b/src/Lidarr.Http/Lidarr.Http.csproj index 1f1519bde..82ec46d69 100644 --- a/src/Lidarr.Http/Lidarr.Http.csproj +++ b/src/Lidarr.Http/Lidarr.Http.csproj @@ -4,9 +4,7 @@ - - - + diff --git a/src/Lidarr.Http/LidarrBootstrapper.cs b/src/Lidarr.Http/LidarrBootstrapper.cs deleted file mode 100644 index 62506a51a..000000000 --- a/src/Lidarr.Http/LidarrBootstrapper.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Linq; -using Lidarr.Http.Extensions.Pipelines; -using Nancy; -using Nancy.Bootstrapper; -using Nancy.Diagnostics; -using Nancy.Responses.Negotiation; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Instrumentation; -using NzbDrone.Core.Instrumentation; -using TinyIoC; - -namespace Lidarr.Http -{ - public class LidarrBootstrapper : TinyIoCNancyBootstrapper - { - private readonly TinyIoCContainer _tinyIoCContainer; - private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(LidarrBootstrapper)); - - public LidarrBootstrapper(TinyIoCContainer tinyIoCContainer) - { - _tinyIoCContainer = tinyIoCContainer; - } - - protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) - { - Logger.Info("Starting Web Server"); - - if (RuntimeInfo.IsProduction) - { - DiagnosticsHook.Disable(pipelines); - } - - RegisterPipelines(pipelines); - - container.Resolve().Register(); - } - - private void RegisterPipelines(IPipelines pipelines) - { - var pipelineRegistrars = _tinyIoCContainer.ResolveAll().OrderBy(v => v.Order).ToList(); - - foreach (var registerNancyPipeline in pipelineRegistrars) - { - registerNancyPipeline.Register(pipelines); - } - } - - protected override TinyIoCContainer GetApplicationContainer() - { - return _tinyIoCContainer; - } - - protected override Func InternalConfiguration - { - get - { - // We don't support Xml Serialization atm - return NancyInternalConfiguration.WithOverrides(x => - { - x.ResponseProcessors.Remove(typeof(ViewProcessor)); - x.ResponseProcessors.Remove(typeof(XmlProcessor)); - }); - } - } - - public override void Configure(Nancy.Configuration.INancyEnvironment environment) - { - environment.Diagnostics(password: @"password"); - } - - protected override byte[] FavIcon => null; - } -} diff --git a/src/Lidarr.Http/LidarrModule.cs b/src/Lidarr.Http/LidarrModule.cs deleted file mode 100644 index 9af4053df..000000000 --- a/src/Lidarr.Http/LidarrModule.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Nancy; -using Nancy.Responses.Negotiation; - -namespace Lidarr.Http -{ - public abstract class LidarrModule : NancyModule - { - protected LidarrModule(string resource) - : base(resource) - { - } - - protected Negotiator ResponseWithCode(object model, HttpStatusCode statusCode) - { - return Negotiate.WithModel(model).WithStatusCode(statusCode); - } - } -} diff --git a/src/Lidarr.Http/LidarrRestModule.cs b/src/Lidarr.Http/LidarrRestModule.cs deleted file mode 100644 index 1984a5e0f..000000000 --- a/src/Lidarr.Http/LidarrRestModule.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using Lidarr.Http.REST; -using Lidarr.Http.Validation; -using NzbDrone.Core.Datastore; - -namespace Lidarr.Http -{ - public abstract class LidarrRestModule : RestModule - where TResource : RestResource, new() - { - protected string Resource { get; private set; } - - private static string BaseUrl() - { - var isV1 = typeof(TResource).Namespace.Contains(".V1."); - if (isV1) - { - return "/api/v1/"; - } - - return "/api/"; - } - - private static string ResourceName() - { - return new TResource().ResourceName.Trim('/').ToLower(); - } - - protected LidarrRestModule() - : this(ResourceName()) - { - } - - protected LidarrRestModule(string resource) - : base(BaseUrl() + resource.Trim('/').ToLower()) - { - Resource = resource; - PostValidator.RuleFor(r => r.Id).IsZero(); - PutValidator.RuleFor(r => r.Id).ValidId(); - } - - protected PagingResource ApplyToPage(Func, PagingSpec> function, PagingSpec pagingSpec, Converter mapper) - { - pagingSpec = function(pagingSpec); - - return new PagingResource - { - Page = pagingSpec.Page, - PageSize = pagingSpec.PageSize, - SortDirection = pagingSpec.SortDirection, - SortKey = pagingSpec.SortKey, - TotalRecords = pagingSpec.TotalRecords, - Records = pagingSpec.Records.ConvertAll(mapper) - }; - } - } -} diff --git a/src/Lidarr.Http/Middleware/CacheHeaderMiddleware.cs b/src/Lidarr.Http/Middleware/CacheHeaderMiddleware.cs new file mode 100644 index 000000000..2a754cdf8 --- /dev/null +++ b/src/Lidarr.Http/Middleware/CacheHeaderMiddleware.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Lidarr.Http.Extensions; +using Microsoft.AspNetCore.Http; + +namespace Lidarr.Http.Middleware +{ + public class CacheHeaderMiddleware + { + private readonly RequestDelegate _next; + private readonly ICacheableSpecification _cacheableSpecification; + + public CacheHeaderMiddleware(RequestDelegate next, ICacheableSpecification cacheableSpecification) + { + _next = next; + _cacheableSpecification = cacheableSpecification; + } + + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Method != "OPTIONS") + { + if (_cacheableSpecification.IsCacheable(context)) + { + context.Response.Headers.EnableCache(); + } + else + { + context.Response.Headers.DisableCache(); + } + } + + await _next(context); + } + } +} diff --git a/src/Lidarr.Http/Middleware/CacheableSpecification.cs b/src/Lidarr.Http/Middleware/CacheableSpecification.cs new file mode 100644 index 000000000..21151b5a2 --- /dev/null +++ b/src/Lidarr.Http/Middleware/CacheableSpecification.cs @@ -0,0 +1,74 @@ +using System; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; + +namespace Lidarr.Http.Middleware +{ + public interface ICacheableSpecification + { + bool IsCacheable(HttpContext context); + } + + public class CacheableSpecification : ICacheableSpecification + { + public bool IsCacheable(HttpContext context) + { + if (!RuntimeInfo.IsProduction) + { + return false; + } + + if (context.Request.Query.ContainsKey("h")) + { + return true; + } + + if (context.Request.Path.StartsWithSegments("/api", StringComparison.CurrentCultureIgnoreCase)) + { + if (context.Request.Path.ToString().ContainsIgnoreCase("/MediaCover")) + { + return true; + } + + return false; + } + + if (context.Request.Path.StartsWithSegments("/signalr", StringComparison.CurrentCultureIgnoreCase)) + { + return false; + } + + if (context.Request.Path.Equals("/index.js")) + { + return false; + } + + if (context.Request.Path.Equals("/initialize.js")) + { + return false; + } + + if (context.Request.Path.StartsWithSegments("/feed", StringComparison.CurrentCultureIgnoreCase)) + { + return false; + } + + if (context.Request.Path.StartsWithSegments("/log", StringComparison.CurrentCultureIgnoreCase) && + context.Request.Path.ToString().EndsWith(".txt", StringComparison.CurrentCultureIgnoreCase)) + { + return false; + } + + if (context.Response != null) + { + if (context.Response.ContentType?.Contains("text/html") ?? false || context.Response.StatusCode >= 400) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/Lidarr.Http/Middleware/IfModifiedMiddleware.cs b/src/Lidarr.Http/Middleware/IfModifiedMiddleware.cs new file mode 100644 index 000000000..4aef568ee --- /dev/null +++ b/src/Lidarr.Http/Middleware/IfModifiedMiddleware.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Threading.Tasks; +using Lidarr.Http.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.StaticFiles; + +namespace Lidarr.Http.Middleware +{ + public class IfModifiedMiddleware + { + private readonly RequestDelegate _next; + private readonly ICacheableSpecification _cacheableSpecification; + private readonly IContentTypeProvider _mimeTypeProvider; + + public IfModifiedMiddleware(RequestDelegate next, ICacheableSpecification cacheableSpecification) + { + _next = next; + _cacheableSpecification = cacheableSpecification; + + _mimeTypeProvider = new FileExtensionContentTypeProvider(); + } + + public async Task InvokeAsync(HttpContext context) + { + if (_cacheableSpecification.IsCacheable(context) && context.Request.Headers["IfModifiedSince"].Any()) + { + context.Response.StatusCode = 304; + context.Response.Headers.EnableCache(); + + if (!_mimeTypeProvider.TryGetContentType(context.Request.Path.ToString(), out var mimeType)) + { + mimeType = "application/octet-stream"; + } + + context.Response.ContentType = mimeType; + + return; + } + + await _next(context); + } + } +} diff --git a/src/Lidarr.Http/Middleware/LoggingMiddleware.cs b/src/Lidarr.Http/Middleware/LoggingMiddleware.cs new file mode 100644 index 000000000..3218da38c --- /dev/null +++ b/src/Lidarr.Http/Middleware/LoggingMiddleware.cs @@ -0,0 +1,92 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Lidarr.Http.ErrorManagement; +using Lidarr.Http.Extensions; +using Microsoft.AspNetCore.Http; +using NLog; +using NzbDrone.Common.Extensions; + +namespace Lidarr.Http.Middleware +{ + public class LoggingMiddleware + { + private static readonly Logger _loggerHttp = LogManager.GetLogger("Http"); + private static readonly Logger _loggerApi = LogManager.GetLogger("Api"); + private static int _requestSequenceID; + + private readonly LidarrErrorPipeline _errorHandler; + private readonly RequestDelegate _next; + + public LoggingMiddleware(RequestDelegate next, + LidarrErrorPipeline errorHandler) + { + _next = next; + _errorHandler = errorHandler; + } + + public async Task InvokeAsync(HttpContext context) + { + LogStart(context); + + await _next(context); + + LogEnd(context); + } + + private void LogStart(HttpContext context) + { + var id = Interlocked.Increment(ref _requestSequenceID); + + context.Items["ApiRequestSequenceID"] = id; + context.Items["ApiRequestStartTime"] = DateTime.UtcNow; + + var reqPath = GetRequestPathAndQuery(context.Request); + + _loggerHttp.Trace("Req: {0} [{1}] {2} (from {3})", id, context.Request.Method, reqPath, GetOrigin(context)); + } + + private void LogEnd(HttpContext context) + { + var id = (int)context.Items["ApiRequestSequenceID"]; + var startTime = (DateTime)context.Items["ApiRequestStartTime"]; + + var endTime = DateTime.UtcNow; + var duration = endTime - startTime; + + var reqPath = GetRequestPathAndQuery(context.Request); + + _loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, context.Response.StatusCode, (HttpStatusCode)context.Response.StatusCode, (int)duration.TotalMilliseconds); + + if (context.Request.IsApiRequest()) + { + _loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, context.Response.StatusCode, (HttpStatusCode)context.Response.StatusCode, (int)duration.TotalMilliseconds); + } + } + + private static string GetRequestPathAndQuery(HttpRequest request) + { + if (request.QueryString.Value.IsNotNullOrWhiteSpace() && request.QueryString.Value != "?") + { + return string.Concat(request.Path, request.QueryString); + } + else + { + return request.Path; + } + } + + private static string GetOrigin(HttpContext context) + { + if (context.Request.Headers["UserAgent"].ToString().IsNullOrWhiteSpace()) + { + return context.GetRemoteIP(); + } + else + { + return $"{context.GetRemoteIP()} {context.Request.Headers["UserAgent"]}"; + } + } + } +} diff --git a/src/Lidarr.Http/Middleware/UrlBaseMiddleware.cs b/src/Lidarr.Http/Middleware/UrlBaseMiddleware.cs new file mode 100644 index 000000000..327f2670d --- /dev/null +++ b/src/Lidarr.Http/Middleware/UrlBaseMiddleware.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.Extensions; + +namespace Lidarr.Http.Middleware +{ + public class UrlBaseMiddleware + { + private readonly RequestDelegate _next; + private readonly string _urlBase; + + public UrlBaseMiddleware(RequestDelegate next, string urlBase) + { + _next = next; + _urlBase = urlBase; + } + + public async Task InvokeAsync(HttpContext context) + { + if (_urlBase.IsNotNullOrWhiteSpace() && context.Request.PathBase.Value.IsNullOrWhiteSpace()) + { + context.Response.Redirect($"{_urlBase}{context.Request.Path}{context.Request.QueryString}"); + return; + } + + await _next(context); + } + } +} diff --git a/src/Lidarr.Http/Middleware/VersionMiddleware.cs b/src/Lidarr.Http/Middleware/VersionMiddleware.cs new file mode 100644 index 000000000..9c611024c --- /dev/null +++ b/src/Lidarr.Http/Middleware/VersionMiddleware.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.EnvironmentInfo; + +namespace Lidarr.Http.Middleware +{ + public class VersionMiddleware + { + private const string VERSIONHEADER = "X-ApplicationVersion"; + + private readonly RequestDelegate _next; + private readonly string _version; + + public VersionMiddleware(RequestDelegate next) + { + _next = next; + _version = BuildInfo.Version.ToString(); + } + + public async Task InvokeAsync(HttpContext context) + { + if (!context.Response.Headers.ContainsKey(VERSIONHEADER)) + { + context.Response.Headers.Add(VERSIONHEADER, _version); + } + + await _next(context); + } + } +} diff --git a/src/Lidarr.Http/REST/Attributes/RestDeleteByIdAttribute.cs b/src/Lidarr.Http/REST/Attributes/RestDeleteByIdAttribute.cs new file mode 100644 index 000000000..21f3eb987 --- /dev/null +++ b/src/Lidarr.Http/REST/Attributes/RestDeleteByIdAttribute.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Lidarr.Http.REST.Attributes +{ + [AttributeUsage(AttributeTargets.Method)] + public class RestDeleteByIdAttribute : HttpDeleteAttribute + { + public RestDeleteByIdAttribute() + : base("{id:int}") + { + } + } +} diff --git a/src/Lidarr.Http/REST/Attributes/RestGetByIdAttribute.cs b/src/Lidarr.Http/REST/Attributes/RestGetByIdAttribute.cs new file mode 100644 index 000000000..47e75560b --- /dev/null +++ b/src/Lidarr.Http/REST/Attributes/RestGetByIdAttribute.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Lidarr.Http.REST.Attributes +{ + [AttributeUsage(AttributeTargets.Method)] + public class RestGetByIdAttribute : ActionFilterAttribute, IActionHttpMethodProvider, IRouteTemplateProvider + { + public override void OnActionExecuting(ActionExecutingContext context) + { + Console.WriteLine($"OnExecuting {context.Controller.GetType()} {context.ActionDescriptor.DisplayName}"); + } + + public IEnumerable HttpMethods => new[] { "GET" }; + public string Template => "{id:int}"; + public new int? Order => 0; + public string Name { get; } + } +} diff --git a/src/Lidarr.Http/REST/Attributes/RestPostByIdAttribute.cs b/src/Lidarr.Http/REST/Attributes/RestPostByIdAttribute.cs new file mode 100644 index 000000000..5f782ab40 --- /dev/null +++ b/src/Lidarr.Http/REST/Attributes/RestPostByIdAttribute.cs @@ -0,0 +1,10 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Lidarr.Http.REST.Attributes +{ + [AttributeUsage(AttributeTargets.Method)] + public class RestPostByIdAttribute : HttpPostAttribute + { + } +} diff --git a/src/Lidarr.Http/REST/Attributes/RestPutByIdAttribute.cs b/src/Lidarr.Http/REST/Attributes/RestPutByIdAttribute.cs new file mode 100644 index 000000000..f0a6673c8 --- /dev/null +++ b/src/Lidarr.Http/REST/Attributes/RestPutByIdAttribute.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Lidarr.Http.REST.Attributes +{ + [AttributeUsage(AttributeTargets.Method)] + public class RestPutByIdAttribute : HttpPutAttribute + { + public RestPutByIdAttribute() + : base("{id:int?}") + { + } + } +} diff --git a/src/Lidarr.Http/REST/Attributes/SkipValidationAttribute.cs b/src/Lidarr.Http/REST/Attributes/SkipValidationAttribute.cs new file mode 100644 index 000000000..dc018a2fd --- /dev/null +++ b/src/Lidarr.Http/REST/Attributes/SkipValidationAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace Lidarr.Http.REST.Attributes +{ + [AttributeUsage(AttributeTargets.Method)] + public class SkipValidationAttribute : Attribute + { + public SkipValidationAttribute(bool skip = true, bool skipShared = true) + { + Skip = skip; + SkipShared = skipShared; + } + + public bool Skip { get; } + public bool SkipShared { get; } + } +} diff --git a/src/Lidarr.Http/REST/BadRequestException.cs b/src/Lidarr.Http/REST/BadRequestException.cs index 4502935c7..770d34a56 100644 --- a/src/Lidarr.Http/REST/BadRequestException.cs +++ b/src/Lidarr.Http/REST/BadRequestException.cs @@ -1,5 +1,5 @@ -using Lidarr.Http.Exceptions; -using Nancy; +using System.Net; +using Lidarr.Http.Exceptions; namespace Lidarr.Http.REST { diff --git a/src/Lidarr.Http/REST/MethodNotAllowedException.cs b/src/Lidarr.Http/REST/MethodNotAllowedException.cs index b5fca7776..787af909d 100644 --- a/src/Lidarr.Http/REST/MethodNotAllowedException.cs +++ b/src/Lidarr.Http/REST/MethodNotAllowedException.cs @@ -1,5 +1,5 @@ -using Lidarr.Http.Exceptions; -using Nancy; +using System.Net; +using Lidarr.Http.Exceptions; namespace Lidarr.Http.REST { diff --git a/src/Lidarr.Http/REST/NotFoundException.cs b/src/Lidarr.Http/REST/NotFoundException.cs index edf4bf8e3..4495c212b 100644 --- a/src/Lidarr.Http/REST/NotFoundException.cs +++ b/src/Lidarr.Http/REST/NotFoundException.cs @@ -1,5 +1,5 @@ -using Lidarr.Http.Exceptions; -using Nancy; +using System.Net; +using Lidarr.Http.Exceptions; namespace Lidarr.Http.REST { diff --git a/src/Lidarr.Http/REST/RestController.cs b/src/Lidarr.Http/REST/RestController.cs new file mode 100644 index 000000000..b75b13820 --- /dev/null +++ b/src/Lidarr.Http/REST/RestController.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using Lidarr.Http.REST.Attributes; +using Lidarr.Http.Validation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using NzbDrone.Core.Datastore; + +namespace Lidarr.Http.REST +{ + public abstract class RestController : Controller + where TResource : RestResource, new() + { + private static readonly List VALIDATE_ID_ATTRIBUTES = new List { typeof(RestPutByIdAttribute), typeof(RestDeleteByIdAttribute) }; + + protected ResourceValidator PostValidator { get; private set; } + protected ResourceValidator PutValidator { get; private set; } + protected ResourceValidator SharedValidator { get; private set; } + + protected void ValidateId(int id) + { + if (id <= 0) + { + throw new BadRequestException(id + " is not a valid ID"); + } + } + + protected RestController() + { + PostValidator = new ResourceValidator(); + PutValidator = new ResourceValidator(); + SharedValidator = new ResourceValidator(); + + PutValidator.RuleFor(r => r.Id).ValidId(); + } + + [RestGetById] + public abstract TResource GetResourceById(int id); + + public override void OnActionExecuting(ActionExecutingContext context) + { + var descriptor = context.ActionDescriptor as ControllerActionDescriptor; + + var skipAttribute = (SkipValidationAttribute)Attribute.GetCustomAttribute(descriptor.MethodInfo, typeof(SkipValidationAttribute), true); + var skipValidate = skipAttribute?.Skip ?? false; + var skipShared = skipAttribute?.SkipShared ?? false; + + if (Request.Method == "POST" || Request.Method == "PUT") + { + var resourceArgs = context.ActionArguments.Values.Where(x => x.GetType() == typeof(TResource)) + .Select(x => x as TResource) + .ToList(); + + foreach (var resource in resourceArgs) + { + ValidateResource(resource, skipValidate, skipShared); + } + } + + var attributes = descriptor.MethodInfo.CustomAttributes; + if (attributes.Any(x => VALIDATE_ID_ATTRIBUTES.Contains(x.GetType())) && !skipValidate) + { + if (context.ActionArguments.TryGetValue("id", out var idObj)) + { + ValidateId((int)idObj); + } + } + + base.OnActionExecuting(context); + } + + public override void OnActionExecuted(ActionExecutedContext context) + { + var descriptor = context.ActionDescriptor as ControllerActionDescriptor; + + var attributes = descriptor.MethodInfo.CustomAttributes; + + if (context.Exception?.GetType() == typeof(ModelNotFoundException) && + attributes.Any(x => x.AttributeType == typeof(RestGetByIdAttribute))) + { + context.Result = new NotFoundResult(); + } + } + + protected void ValidateResource(TResource resource, bool skipValidate = false, bool skipSharedValidate = false) + { + if (resource == null) + { + throw new BadRequestException("Request body can't be empty"); + } + + var errors = new List(); + + if (!skipSharedValidate) + { + errors.AddRange(SharedValidator.Validate(resource).Errors); + } + + if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Path.ToString().EndsWith("/test", StringComparison.InvariantCultureIgnoreCase)) + { + errors.AddRange(PostValidator.Validate(resource).Errors); + } + else if (Request.Method.Equals("PUT", StringComparison.InvariantCultureIgnoreCase)) + { + errors.AddRange(PutValidator.Validate(resource).Errors); + } + + if (errors.Any()) + { + throw new ValidationException(errors); + } + } + + protected ActionResult Accepted(int id) + { + var result = GetResourceById(id); + return AcceptedAtAction(nameof(GetResourceById), new { id = id }, result); + } + + protected ActionResult Created(int id) + { + var result = GetResourceById(id); + return CreatedAtAction(nameof(GetResourceById), new { id = id }, result); + } + } +} diff --git a/src/Lidarr.Http/LidarrRestModuleWithSignalR.cs b/src/Lidarr.Http/REST/RestControllerWithSignalR.cs similarity index 76% rename from src/Lidarr.Http/LidarrRestModuleWithSignalR.cs rename to src/Lidarr.Http/REST/RestControllerWithSignalR.cs index abd024c1c..ee7e8a56f 100644 --- a/src/Lidarr.Http/LidarrRestModuleWithSignalR.cs +++ b/src/Lidarr.Http/REST/RestControllerWithSignalR.cs @@ -1,28 +1,35 @@ -using Lidarr.Http.REST; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.SignalR; -namespace Lidarr.Http +namespace Lidarr.Http.REST { - public abstract class LidarrRestModuleWithSignalR : LidarrRestModule, IHandle> + public abstract class RestControllerWithSignalR : RestController, IHandle> where TResource : RestResource, new() where TModel : ModelBase, new() { + protected string Resource { get; } private readonly IBroadcastSignalRMessage _signalRBroadcaster; - protected LidarrRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster) + protected RestControllerWithSignalR(IBroadcastSignalRMessage signalRBroadcaster) { _signalRBroadcaster = signalRBroadcaster; - } - protected LidarrRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster, string resource) - : base(resource) - { - _signalRBroadcaster = signalRBroadcaster; + var apiAttribute = GetType().GetCustomAttribute(); + if (apiAttribute != null && apiAttribute.Resource != VersionedApiControllerAttribute.CONTROLLER_RESOURCE) + { + Resource = apiAttribute.Resource; + } + else + { + Resource = new TResource().ResourceName.Trim('/'); + } } + [NonAction] public void Handle(ModelEvent message) { if (!_signalRBroadcaster.IsConnected) diff --git a/src/Lidarr.Http/REST/RestModule.cs b/src/Lidarr.Http/REST/RestModule.cs deleted file mode 100644 index bb2161787..000000000 --- a/src/Lidarr.Http/REST/RestModule.cs +++ /dev/null @@ -1,373 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using FluentValidation; -using FluentValidation.Results; -using Lidarr.Http.Extensions; -using Nancy; -using Nancy.Responses.Negotiation; -using NzbDrone.Core.Datastore; - -namespace Lidarr.Http.REST -{ - public abstract class RestModule : NancyModule - where TResource : RestResource, new() - { - private const string ROOT_ROUTE = "/"; - private const string ID_ROUTE = @"/(?[\d]{1,10})"; - - // See src/Lidarr.Api.V1/Queue/QueueModule.cs - private static readonly HashSet VALID_SORT_KEYS = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "artists.sortname", //Workaround artists table properties not being added on isValidSortKey call - "timeleft", - "estimatedCompletionTime", - "protocol", - "indexer", - "downloadClient", - "quality", - "status", - "title", - "progress" - }; - - private readonly HashSet _excludedKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - "page", - "pageSize", - "sortKey", - "sortDirection", - "filterKey", - "filterValue", - }; - - private Action _deleteResource; - private Func _getResourceById; - private Func> _getResourceAll; - private Func, PagingResource> _getResourcePaged; - private Func _getResourceSingle; - private Func _createResource; - private Action _updateResource; - - protected ResourceValidator PostValidator { get; private set; } - protected ResourceValidator PutValidator { get; private set; } - protected ResourceValidator SharedValidator { get; private set; } - - protected void ValidateId(int id) - { - if (id <= 0) - { - throw new BadRequestException(id + " is not a valid ID"); - } - } - - protected RestModule(string modulePath) - : base(modulePath) - { - ValidateModule(); - - PostValidator = new ResourceValidator(); - PutValidator = new ResourceValidator(); - SharedValidator = new ResourceValidator(); - } - - private void ValidateModule() - { - if (GetResourceById != null) - { - return; - } - - if (CreateResource != null || UpdateResource != null) - { - throw new InvalidOperationException("GetResourceById route must be defined before defining Create/Update routes."); - } - } - - protected Action DeleteResource - { - private get - { - return _deleteResource; - } - - set - { - _deleteResource = value; - Delete(ID_ROUTE, options => - { - ValidateId(options.Id); - DeleteResource((int)options.Id); - - return new object(); - }); - } - } - - protected Func GetResourceById - { - get - { - return _getResourceById; - } - - set - { - _getResourceById = value; - Get(ID_ROUTE, options => - { - ValidateId(options.Id); - try - { - var resource = GetResourceById((int)options.Id); - - if (resource == null) - { - return new NotFoundResponse(); - } - - return resource; - } - catch (ModelNotFoundException) - { - return new NotFoundResponse(); - } - }); - } - } - - protected Func> GetResourceAll - { - private get - { - return _getResourceAll; - } - - set - { - _getResourceAll = value; - Get(ROOT_ROUTE, options => - { - var resource = GetResourceAll(); - return resource; - }); - } - } - - protected Func, PagingResource> GetResourcePaged - { - private get - { - return _getResourcePaged; - } - - set - { - _getResourcePaged = value; - Get(ROOT_ROUTE, options => - { - var resource = GetResourcePaged(ReadPagingResourceFromRequest()); - return resource; - }); - } - } - - protected Func GetResourceSingle - { - private get - { - return _getResourceSingle; - } - - set - { - _getResourceSingle = value; - Get(ROOT_ROUTE, options => - { - var resource = GetResourceSingle(); - return resource; - }); - } - } - - protected Func CreateResource - { - private get - { - return _createResource; - } - - set - { - _createResource = value; - Post(ROOT_ROUTE, options => - { - var id = CreateResource(ReadResourceFromRequest()); - return ResponseWithCode(GetResourceById(id), HttpStatusCode.Created); - }); - } - } - - protected Action UpdateResource - { - private get - { - return _updateResource; - } - - set - { - _updateResource = value; - Put(ROOT_ROUTE, options => - { - var resource = ReadResourceFromRequest(); - UpdateResource(resource); - return ResponseWithCode(GetResourceById(resource.Id), HttpStatusCode.Accepted); - }); - Put(ID_ROUTE, options => - { - var resource = ReadResourceFromRequest(); - resource.Id = options.Id; - UpdateResource(resource); - return ResponseWithCode(GetResourceById(resource.Id), HttpStatusCode.Accepted); - }); - } - } - - protected Negotiator ResponseWithCode(object model, HttpStatusCode statusCode) - { - return Negotiate.WithModel(model).WithStatusCode(statusCode); - } - - protected TResource ReadResourceFromRequest(bool skipValidate = false, bool skipSharedValidate = false) - { - var resource = new TResource(); - - try - { - resource = Request.Body.FromJson(); - } - catch (JsonException e) - { - throw new BadRequestException(e.Message); - } - - if (resource == null) - { - throw new BadRequestException("Request body can't be empty"); - } - - var errors = new List(); - - if (!skipSharedValidate) - { - errors.AddRange(SharedValidator.Validate(resource).Errors); - } - - if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Url.Path.EndsWith("/test", StringComparison.InvariantCultureIgnoreCase)) - { - errors.AddRange(PostValidator.Validate(resource).Errors); - } - else if (Request.Method.Equals("PUT", StringComparison.InvariantCultureIgnoreCase)) - { - errors.AddRange(PutValidator.Validate(resource).Errors); - } - - if (errors.Any()) - { - throw new ValidationException(errors); - } - - return resource; - } - - private PagingResource ReadPagingResourceFromRequest() - { - int pageSize; - int.TryParse(Request.Query.PageSize.ToString(), out pageSize); - if (pageSize == 0) - { - pageSize = 10; - } - - int page; - int.TryParse(Request.Query.Page.ToString(), out page); - if (page == 0) - { - page = 1; - } - - var pagingResource = new PagingResource - { - PageSize = pageSize, - Page = page, - Filters = new List() - }; - - if (Request.Query.SortKey != null) - { - var sortKey = Request.Query.SortKey.ToString(); - - if (!VALID_SORT_KEYS.Contains(sortKey) && - !TableMapping.Mapper.IsValidSortKey(sortKey)) - { - throw new BadRequestException($"Invalid sort key {sortKey}"); - } - - pagingResource.SortKey = sortKey; - - // For backwards compatibility with v2 - if (Request.Query.SortDir != null) - { - pagingResource.SortDirection = Request.Query.SortDir.ToString() - .Equals("Asc", StringComparison.InvariantCultureIgnoreCase) - ? SortDirection.Ascending - : SortDirection.Descending; - } - - // v3 uses SortDirection instead of SortDir to be consistent with every other use of it - if (Request.Query.SortDirection != null) - { - pagingResource.SortDirection = Request.Query.SortDirection.ToString() - .Equals("ascending", StringComparison.InvariantCultureIgnoreCase) - ? SortDirection.Ascending - : SortDirection.Descending; - } - } - - // For backwards compatibility with v2 - if (Request.Query.FilterKey != null) - { - var filter = new PagingResourceFilter - { - Key = Request.Query.FilterKey.ToString() - }; - - if (Request.Query.FilterValue != null) - { - filter.Value = Request.Query.FilterValue?.ToString(); - } - - pagingResource.Filters.Add(filter); - } - - // v3 uses filters in key=value format - foreach (var key in Request.Query) - { - if (_excludedKeys.Contains(key)) - { - continue; - } - - pagingResource.Filters.Add(new PagingResourceFilter - { - Key = key, - Value = Request.Query[key] - }); - } - - return pagingResource; - } - } -} diff --git a/src/Lidarr.Http/REST/UnsupportedMediaTypeException.cs b/src/Lidarr.Http/REST/UnsupportedMediaTypeException.cs index e13190cc7..8c2c6351d 100644 --- a/src/Lidarr.Http/REST/UnsupportedMediaTypeException.cs +++ b/src/Lidarr.Http/REST/UnsupportedMediaTypeException.cs @@ -1,5 +1,5 @@ +using System.Net; using Lidarr.Http.Exceptions; -using Nancy; namespace Lidarr.Http.REST { diff --git a/src/Lidarr.Http/TinyIoCNancyBootstrapper.cs b/src/Lidarr.Http/TinyIoCNancyBootstrapper.cs deleted file mode 100644 index c4f0a28b0..000000000 --- a/src/Lidarr.Http/TinyIoCNancyBootstrapper.cs +++ /dev/null @@ -1,273 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Nancy; -using Nancy.Bootstrapper; -using Nancy.Configuration; -using Nancy.Diagnostics; -using TinyIoC; - -namespace Lidarr.Http -{ - /// - /// TinyIoC bootstrapper - registers default route resolver and registers itself as - /// INancyModuleCatalog for resolving modules but behaviour can be overridden if required. - /// - public class TinyIoCNancyBootstrapper : NancyBootstrapperWithRequestContainerBase - { - /// - /// Default assemblies that are ignored for autoregister - /// - public static IEnumerable> DefaultAutoRegisterIgnoredAssemblies = new Func[] - { - asm => !asm.FullName.StartsWith("Nancy.", StringComparison.InvariantCulture) - }; - - /// - /// Gets the assemblies to ignore when autoregistering the application container - /// Return true from the delegate to ignore that particular assembly, returning false - /// does not mean the assembly *will* be included, a true from another delegate will - /// take precedence. - /// - protected virtual IEnumerable> AutoRegisterIgnoredAssemblies => DefaultAutoRegisterIgnoredAssemblies; - - /// - /// Configures the container using AutoRegister followed by registration - /// of default INancyModuleCatalog and IRouteResolver. - /// - /// Container instance - protected override void ConfigureApplicationContainer(TinyIoCContainer container) - { - AutoRegister(container, AutoRegisterIgnoredAssemblies); - } - - /// - /// Resolve INancyEngine - /// - /// INancyEngine implementation - protected override sealed INancyEngine GetEngineInternal() - { - return ApplicationContainer.Resolve(); - } - - // Summary: - // Gets the Nancy.Configuration.INancyEnvironmentConfigurator used by th. - // Returns: - // An Nancy.Configuration.INancyEnvironmentConfigurator instance. - protected override INancyEnvironmentConfigurator GetEnvironmentConfigurator() - { - return ApplicationContainer.Resolve(); - } - - // Summary: - // Get the Nancy.Configuration.INancyEnvironment instance. - // Returns: - // An configured Nancy.Configuration.INancyEnvironment instance. - // Remarks: - // The boostrapper must be initialised (Nancy.Bootstrapper.INancyBootstrapper.Initialise) - // prior to calling this. - public override INancyEnvironment GetEnvironment() - { - return ApplicationContainer.Resolve(); - } - - // Summary: - // Registers an Nancy.Configuration.INancyEnvironment instance in the container. - // Parameters: - // container: - // The container to register into. - // environment: - // The Nancy.Configuration.INancyEnvironment instance to register. - protected override void RegisterNancyEnvironment(TinyIoCContainer container, INancyEnvironment environment) - { - ApplicationContainer.Register(environment); - } - - /// - /// Create a default, unconfigured, container - /// - /// Container instance - protected override TinyIoCContainer GetApplicationContainer() - { - return new TinyIoCContainer(); - } - - /// - /// Register the bootstrapper's implemented types into the container. - /// This is necessary so a user can pass in a populated container but not have - /// to take the responsibility of registering things like INancyModuleCatalog manually. - /// - /// Application container to register into - protected override sealed void RegisterBootstrapperTypes(TinyIoCContainer applicationContainer) - { - applicationContainer.Register(this); - } - - /// - /// Register the default implementations of internally used types into the container as singletons - /// - /// Container to register into - /// Type registrations to register - protected override sealed void RegisterTypes(TinyIoCContainer container, IEnumerable typeRegistrations) - { - foreach (var typeRegistration in typeRegistrations) - { - switch (typeRegistration.Lifetime) - { - case Lifetime.Transient: - container.Register(typeRegistration.RegistrationType, typeRegistration.ImplementationType).AsMultiInstance(); - break; - case Lifetime.Singleton: - container.Register(typeRegistration.RegistrationType, typeRegistration.ImplementationType).AsSingleton(); - break; - case Lifetime.PerRequest: - throw new InvalidOperationException("Unable to directly register a per request lifetime."); - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - /// - /// Register the various collections into the container as singletons to later be resolved - /// by IEnumerable{Type} constructor dependencies. - /// - /// Container to register into - /// Collection type registrations to register - protected override sealed void RegisterCollectionTypes(TinyIoCContainer container, IEnumerable collectionTypeRegistrations) - { - foreach (var collectionTypeRegistration in collectionTypeRegistrations) - { - switch (collectionTypeRegistration.Lifetime) - { - case Lifetime.Transient: - container.RegisterMultiple(collectionTypeRegistration.RegistrationType, collectionTypeRegistration.ImplementationTypes).AsMultiInstance(); - break; - case Lifetime.Singleton: - container.RegisterMultiple(collectionTypeRegistration.RegistrationType, collectionTypeRegistration.ImplementationTypes).AsSingleton(); - break; - case Lifetime.PerRequest: - throw new InvalidOperationException("Unable to directly register a per request lifetime."); - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - /// - /// Register the given module types into the container - /// - /// Container to register into - /// NancyModule types - protected override sealed void RegisterRequestContainerModules(TinyIoCContainer container, IEnumerable moduleRegistrationTypes) - { - foreach (var moduleRegistrationType in moduleRegistrationTypes) - { - container.Register( - typeof(INancyModule), - moduleRegistrationType.ModuleType, - moduleRegistrationType.ModuleType.FullName). - AsSingleton(); - } - } - - /// - /// Register the given instances into the container - /// - /// Container to register into - /// Instance registration types - protected override void RegisterInstances(TinyIoCContainer container, IEnumerable instanceRegistrations) - { - foreach (var instanceRegistration in instanceRegistrations) - { - container.Register( - instanceRegistration.RegistrationType, - instanceRegistration.Implementation); - } - } - - /// - /// Creates a per request child/nested container - /// - /// Current context - /// Request container instance - protected override TinyIoCContainer CreateRequestContainer(NancyContext context) - { - return ApplicationContainer.GetChildContainer(); - } - - /// - /// Gets the diagnostics for initialisation - /// - /// IDiagnostics implementation - protected override IDiagnostics GetDiagnostics() - { - return ApplicationContainer.Resolve(); - } - - /// - /// Gets all registered startup tasks - /// - /// An instance containing instances. - protected override IEnumerable GetApplicationStartupTasks() - { - return ApplicationContainer.ResolveAll(false); - } - - /// - /// Gets all registered request startup tasks - /// - /// An instance containing instances. - protected override IEnumerable RegisterAndGetRequestStartupTasks(TinyIoCContainer container, Type[] requestStartupTypes) - { - container.RegisterMultiple(typeof(IRequestStartup), requestStartupTypes); - - return container.ResolveAll(false); - } - - /// - /// Gets all registered application registration tasks - /// - /// An instance containing instances. - protected override IEnumerable GetRegistrationTasks() - { - return ApplicationContainer.ResolveAll(false); - } - - /// - /// Retrieve all module instances from the container - /// - /// Container to use - /// Collection of NancyModule instances - protected override sealed IEnumerable GetAllModules(TinyIoCContainer container) - { - var nancyModules = container.ResolveAll(false); - return nancyModules; - } - - /// - /// Retrieve a specific module instance from the container - /// - /// Container to use - /// Type of the module - /// NancyModule instance - protected override sealed INancyModule GetModule(TinyIoCContainer container, Type moduleType) - { - container.Register(typeof(INancyModule), moduleType); - - return container.Resolve(); - } - - /// - /// Executes auto registation with the given container. - /// - /// Container instance - private static void AutoRegister(TinyIoCContainer container, IEnumerable> ignoredAssemblies) - { - var assembly = typeof(NancyEngine).Assembly; - - container.AutoRegister(AppDomain.CurrentDomain.GetAssemblies().Where(a => !ignoredAssemblies.Any(ia => ia(a))), DuplicateImplementationActions.RegisterMultiple, t => t.Assembly != assembly); - } - } -} diff --git a/src/Lidarr.Http/Validation/DuplicateEndpointDetector.cs b/src/Lidarr.Http/Validation/DuplicateEndpointDetector.cs new file mode 100644 index 000000000..9121b3ec5 --- /dev/null +++ b/src/Lidarr.Http/Validation/DuplicateEndpointDetector.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using ImpromptuInterface; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; +using Microsoft.Extensions.DependencyInjection; + +namespace Lidarr.Http.Validation +{ + public interface IDfaMatcherBuilder + { + void AddEndpoint(RouteEndpoint endpoint); + object BuildDfaTree(bool includeLabel = false); + } + + // https://github.com/dotnet/aspnetcore/blob/cc3d47f5501cdfae3e5b5be509ef2c0fb8cca069/src/Http/Routing/src/Matching/DfaNode.cs + public interface IDfaNode + { + string Label { get; set; } + List Matches { get; } + IDictionary Literals { get; } + object Parameters { get; } + object CatchAll { get; } + IDictionary PolicyEdges { get; } + } + + public class DuplicateEndpointDetector + { + private readonly IServiceProvider _services; + + public DuplicateEndpointDetector(IServiceProvider services) + { + _services = services; + } + + public Dictionary> GetDuplicateEndpoints(EndpointDataSource dataSource) + { + // get the DfaMatcherBuilder - internal, so needs reflection :( + var matcherBuilder = typeof(IEndpointSelectorPolicy).Assembly + .GetType("Microsoft.AspNetCore.Routing.Matching.DfaMatcherBuilder"); + + var rawBuilder = _services.GetRequiredService(matcherBuilder); + var builder = rawBuilder.ActLike(); + + var endpoints = dataSource.Endpoints; + foreach (var t in endpoints) + { + if (t is RouteEndpoint endpoint && (endpoint.Metadata.GetMetadata()?.SuppressMatching ?? false) == false) + { + builder.AddEndpoint(endpoint); + } + } + + // Assign each node a sequential index. + var visited = new Dictionary(); + var duplicates = new Dictionary>(); + + var rawTree = builder.BuildDfaTree(includeLabel: true); + + Visit(rawTree, LogDuplicates); + + return duplicates; + + void LogDuplicates(IDfaNode node) + { + if (!visited.TryGetValue(node, out var label)) + { + label = visited.Count; + visited.Add(node, label); + } + + // We can safely index into visited because this is a post-order traversal, + // all of the children of this node are already in the dictionary. + var filteredMatches = node?.Matches?.Where(x => !x.DisplayName.StartsWith("Lidarr.Http.Frontend.StaticResourceController")).ToList(); + var matchCount = filteredMatches?.Count ?? 0; + if (matchCount > 1) + { + var duplicateEndpoints = filteredMatches.Select(x => x.DisplayName).ToList(); + duplicates[node.Label] = duplicateEndpoints; + } + } + } + + private static void Visit(object rawNode, Action visitor) + { + var node = rawNode.ActLike(); + if (node.Literals?.Values != null) + { + foreach (var dictValue in node.Literals.Values) + { + Visit(dictValue, visitor); + } + } + + // Break cycles + if (node.Parameters != null && !ReferenceEquals(rawNode, node.Parameters)) + { + Visit(node.Parameters, visitor); + } + + // Break cycles + if (node.CatchAll != null && !ReferenceEquals(rawNode, node.CatchAll)) + { + Visit(node.CatchAll, visitor); + } + + if (node.PolicyEdges?.Values != null) + { + foreach (var dictValue in node.PolicyEdges.Values) + { + Visit(dictValue, visitor); + } + } + + visitor(node); + } + } +} diff --git a/src/Lidarr.Http/VersionedApiControllerAttribute.cs b/src/Lidarr.Http/VersionedApiControllerAttribute.cs new file mode 100644 index 000000000..c04b6008c --- /dev/null +++ b/src/Lidarr.Http/VersionedApiControllerAttribute.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Lidarr.Http +{ + public class VersionedApiControllerAttribute : Attribute, IRouteTemplateProvider, IEnableCorsAttribute, IApiBehaviorMetadata + { + public const string API_CORS_POLICY = "ApiCorsPolicy"; + public const string CONTROLLER_RESOURCE = "[controller]"; + + public VersionedApiControllerAttribute(int version, string resource = CONTROLLER_RESOURCE) + { + Resource = resource; + Template = $"api/v{version}/{resource}"; + PolicyName = API_CORS_POLICY; + } + + public string Resource { get; } + public string Template { get; } + public int? Order => 2; + public string Name { get; set; } + public string PolicyName { get; set; } + } + + public class V1ApiControllerAttribute : VersionedApiControllerAttribute + { + public V1ApiControllerAttribute(string resource = "[controller]") + : base(1, resource) + { + } + } +} diff --git a/src/Lidarr.Http/VersionedFeedControllerAttribute.cs b/src/Lidarr.Http/VersionedFeedControllerAttribute.cs new file mode 100644 index 000000000..3444c5736 --- /dev/null +++ b/src/Lidarr.Http/VersionedFeedControllerAttribute.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Lidarr.Http +{ + public class VersionedFeedControllerAttribute : Attribute, IRouteTemplateProvider + { + public VersionedFeedControllerAttribute(int version, string resource = "[controller]") + { + Version = version; + Template = $"feed/v{Version}/{resource}"; + } + + public string Template { get; private set; } + public int? Order => 2; + public string Name { get; set; } + public int Version { get; private set; } + } + + public class V1FeedControllerAttribute : VersionedApiControllerAttribute + { + public V1FeedControllerAttribute(string resource = "[controller]") + : base(1, resource) + { + } + } +} diff --git a/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs b/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs index 48b98cf6d..6b28a17ac 100644 --- a/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs +++ b/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading.Tasks; namespace NzbDrone.Common.Serializer { @@ -15,23 +16,25 @@ namespace NzbDrone.Common.Serializer public static JsonSerializerOptions GetSerializerSettings() { - var serializerSettings = new JsonSerializerOptions - { - AllowTrailingCommas = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNameCaseInsensitive = true, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }; + var settings = new JsonSerializerOptions(); + ApplySerializerSettings(settings); + return settings; + } + + public static void ApplySerializerSettings(JsonSerializerOptions serializerSettings) + { + serializerSettings.AllowTrailingCommas = true; + serializerSettings.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + serializerSettings.PropertyNameCaseInsensitive = true; + serializerSettings.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + serializerSettings.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + serializerSettings.WriteIndented = true; serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true)); serializerSettings.Converters.Add(new STJVersionConverter()); serializerSettings.Converters.Add(new STJHttpUriConverter()); serializerSettings.Converters.Add(new STJTimeSpanConverter()); serializerSettings.Converters.Add(new STJUtcConverter()); - - return serializerSettings; } public static T Deserialize(string json) @@ -84,5 +87,15 @@ namespace NzbDrone.Common.Serializer JsonSerializer.Serialize(writer, (object)model, options); } } + + public static Task SerializeAsync(TModel model, Stream outputStream, JsonSerializerOptions options = null) + { + if (options == null) + { + options = SerializerSettings; + } + + return JsonSerializer.SerializeAsync(outputStream, (object)model, options); + } } } diff --git a/src/NzbDrone.Host/MainAppContainerBuilder.cs b/src/NzbDrone.Host/MainAppContainerBuilder.cs index 00de2fd3e..f2a788f3e 100644 --- a/src/NzbDrone.Host/MainAppContainerBuilder.cs +++ b/src/NzbDrone.Host/MainAppContainerBuilder.cs @@ -1,6 +1,4 @@ using System.Collections.Generic; -using Lidarr.Http; -using Nancy.Bootstrapper; using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.SignalR; @@ -28,8 +26,6 @@ namespace NzbDrone.Host { AutoRegisterImplementations(); - Container.Register(); - if (OsInfo.IsWindows) { Container.Register(); diff --git a/src/NzbDrone.Host/WebHost/ControllerActivator.cs b/src/NzbDrone.Host/WebHost/ControllerActivator.cs new file mode 100644 index 000000000..ad90106fa --- /dev/null +++ b/src/NzbDrone.Host/WebHost/ControllerActivator.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using NzbDrone.Common.Composition; + +namespace NzbDrone.Host +{ + public class ControllerActivator : IControllerActivator + { + private readonly IContainer _container; + + public ControllerActivator(IContainer container) + { + _container = container; + } + + public object Create(ControllerContext context) + { + return _container.Resolve(context.ActionDescriptor.ControllerTypeInfo.AsType()); + } + + public void Release(ControllerContext context, object controller) + { + // Nothing to do + } + } +} diff --git a/src/NzbDrone.Host/WebHost/Middleware/IAspNetCoreMiddleware.cs b/src/NzbDrone.Host/WebHost/Middleware/IAspNetCoreMiddleware.cs deleted file mode 100644 index 8121fd19b..000000000 --- a/src/NzbDrone.Host/WebHost/Middleware/IAspNetCoreMiddleware.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Builder; - -namespace NzbDrone.Host.Middleware -{ - public interface IAspNetCoreMiddleware - { - int Order { get; } - void Attach(IApplicationBuilder appBuilder); - } -} diff --git a/src/NzbDrone.Host/WebHost/Middleware/NancyMiddleware.cs b/src/NzbDrone.Host/WebHost/Middleware/NancyMiddleware.cs deleted file mode 100644 index 489bea524..000000000 --- a/src/NzbDrone.Host/WebHost/Middleware/NancyMiddleware.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Nancy.Bootstrapper; -using Nancy.Owin; - -namespace NzbDrone.Host.Middleware -{ - public class NancyMiddleware : IAspNetCoreMiddleware - { - private readonly INancyBootstrapper _nancyBootstrapper; - - public int Order => 2; - - public NancyMiddleware(INancyBootstrapper nancyBootstrapper) - { - _nancyBootstrapper = nancyBootstrapper; - } - - public void Attach(IApplicationBuilder appBuilder) - { - var options = new NancyOptions - { - Bootstrapper = _nancyBootstrapper, - PerformPassThrough = context => context.Request.Path.StartsWith("/signalr") - }; - - appBuilder.UseOwin(x => x.UseNancy(options)); - } - } -} diff --git a/src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs b/src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs deleted file mode 100644 index 84d967076..000000000 --- a/src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.DependencyInjection; -using NLog; -using NzbDrone.Common.Composition; -using NzbDrone.Core.Configuration; -using NzbDrone.SignalR; - -namespace NzbDrone.Host.Middleware -{ - public class SignalRMiddleware : IAspNetCoreMiddleware - { - private readonly IContainer _container; - private readonly Logger _logger; - private static string API_KEY; - private static string URL_BASE; - public int Order => 1; - - public SignalRMiddleware(IContainer container, - IConfigFileProvider configFileProvider, - Logger logger) - { - _container = container; - _logger = logger; - API_KEY = configFileProvider.ApiKey; - URL_BASE = configFileProvider.UrlBase; - } - - public void Attach(IApplicationBuilder appBuilder) - { - appBuilder.UseWebSockets(); - - appBuilder.Use(async (context, next) => - { - if (context.Request.Path.StartsWithSegments("/signalr") && - !context.Request.Path.Value.EndsWith("/negotiate") && - (!context.Request.Query.ContainsKey("access_token") || - context.Request.Query["access_token"] != API_KEY)) - { - context.Response.StatusCode = 401; - await context.Response.WriteAsync("Unauthorized"); - return; - } - - try - { - await next(); - } - catch (OperationCanceledException e) - { - // Demote the exception to trace logging so users don't worry (as much). - _logger.Trace(e); - } - }); - - appBuilder.UseEndpoints(x => - { - x.MapHub(URL_BASE + "/signalr/messages"); - }); - - // This is a side effect of haing multiple IoC containers, TinyIoC and whatever - // Kestrel/SignalR is using. Ideally we'd have one IoC container, but that's non-trivial with TinyIoC - // TODO: Use a single IoC container if supported for TinyIoC or if we switch to another system (ie Autofac). - var hubContext = appBuilder.ApplicationServices.GetService>(); - _container.Register(hubContext); - } - } -} diff --git a/src/NzbDrone.Host/WebHost/WebHostController.cs b/src/NzbDrone.Host/WebHost/WebHostController.cs index 6320bebc1..30f307fc0 100644 --- a/src/NzbDrone.Host/WebHost/WebHostController.cs +++ b/src/NzbDrone.Host/WebHost/WebHostController.cs @@ -4,42 +4,59 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using Lidarr.Api.V1.System; +using Lidarr.Http; +using Lidarr.Http.Authentication; +using Lidarr.Http.ErrorManagement; +using Lidarr.Http.Frontend; +using Lidarr.Http.Middleware; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog; using NLog.Extensions.Logging; +using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Exceptions; using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; using NzbDrone.Host.AccessControl; -using NzbDrone.Host.Middleware; +using NzbDrone.SignalR; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace NzbDrone.Host { public class WebHostController : IHostController { + private readonly IContainer _container; private readonly IRuntimeInfo _runtimeInfo; private readonly IConfigFileProvider _configFileProvider; private readonly IFirewallAdapter _firewallAdapter; - private readonly IEnumerable _middlewares; + private readonly LidarrErrorPipeline _errorHandler; private readonly Logger _logger; private IWebHost _host; - public WebHostController(IRuntimeInfo runtimeInfo, + public WebHostController(IContainer container, + IRuntimeInfo runtimeInfo, IConfigFileProvider configFileProvider, IFirewallAdapter firewallAdapter, - IEnumerable middlewares, + LidarrErrorPipeline errorHandler, Logger logger) { + _container = container; _runtimeInfo = runtimeInfo; _configFileProvider = configFileProvider; _firewallAdapter = firewallAdapter; - _middlewares = middlewares; + _errorHandler = errorHandler; _logger = logger; } @@ -103,24 +120,125 @@ namespace NzbDrone.Host }) .ConfigureServices(services => { + // So that we can resolve containers with our TinyIoC services + services.AddSingleton(_container); + services.AddSingleton(); + + // Bits used in our custom middleware + services.AddSingleton(_container.Resolve()); + services.AddSingleton(_container.Resolve()); + + // Used in authentication + services.AddSingleton(_container.Resolve()); + + services.AddRouting(options => options.LowercaseUrls = true); + + services.AddResponseCompression(); + + services.AddCors(options => + { + options.AddPolicy(VersionedApiControllerAttribute.API_CORS_POLICY, + builder => + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + + options.AddPolicy("AllowGet", + builder => + builder.AllowAnyOrigin() + .WithMethods("GET", "OPTIONS") + .AllowAnyHeader()); + }); + + services + .AddControllers(options => + { + options.ReturnHttpNotAcceptable = true; + }) + .AddApplicationPart(typeof(SystemController).Assembly) + .AddApplicationPart(typeof(StaticResourceController).Assembly) + .AddJsonOptions(options => + { + STJson.ApplySerializerSettings(options.JsonSerializerOptions); + }); + services .AddSignalR() .AddJsonProtocol(options => { options.PayloadSerializerOptions = STJson.GetSerializerSettings(); }); + + services.AddAuthorization(options => + { + options.AddPolicy("UI", policy => + { + policy.AuthenticationSchemes.Add(_configFileProvider.AuthenticationMethod.ToString()); + policy.RequireAuthenticatedUser(); + }); + + options.AddPolicy("SignalR", policy => + { + policy.AuthenticationSchemes.Add("SignalR"); + policy.RequireAuthenticatedUser(); + }); + + // Require auth on everything except those marked [AllowAnonymous] + options.DefaultPolicy = new AuthorizationPolicyBuilder("API") + .RequireAuthenticatedUser() + .Build(); + }); + + services.AddAppAuthentication(_configFileProvider); }) .Configure(app => { + app.UseMiddleware(); + app.UsePathBase(new PathString(_configFileProvider.UrlBase)); + app.UseExceptionHandler(new ExceptionHandlerOptions + { + AllowStatusCode404Response = true, + ExceptionHandler = _errorHandler.HandleException + }); + app.UseRouting(); + app.UseCors(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseResponseCompression(); app.Properties["host.AppName"] = BuildInfo.AppName; - app.UsePathBase(_configFileProvider.UrlBase); - foreach (var middleWare in _middlewares.OrderBy(c => c.Order)) + app.UseMiddleware(); + app.UseMiddleware(_configFileProvider.UrlBase); + app.UseMiddleware(); + app.UseMiddleware(); + + app.Use((context, next) => { - _logger.Debug("Attaching {0} to host", middleWare.GetType().Name); - middleWare.Attach(app); - } + if (context.Request.Path.StartsWithSegments("/api/v1/command", StringComparison.CurrentCultureIgnoreCase)) + { + context.Request.EnableBuffering(); + } + + return next(); + }); + + app.UseWebSockets(); + + app.UseEndpoints(x => + { + x.MapHub("/signalr/messages").RequireAuthorization("SignalR"); + x.MapControllers(); + }); + + // This is a side effect of haing multiple IoC containers, TinyIoC and whatever + // Kestrel/SignalR is using. Ideally we'd have one IoC container, but that's non-trivial with TinyIoC + // TODO: Use a single IoC container if supported for TinyIoC or if we switch to another system (ie Autofac). + _container.Register(app.ApplicationServices); + _container.Register(app.ApplicationServices.GetService>()); + _container.Register(app.ApplicationServices.GetService()); + _container.Register(app.ApplicationServices.GetService()); + _container.Register(app.ApplicationServices.GetService()); }) .UseContentRoot(Directory.GetCurrentDirectory()) .Build(); diff --git a/src/NzbDrone.Integration.Test/Client/ClientBase.cs b/src/NzbDrone.Integration.Test/Client/ClientBase.cs index 870aef74c..17dd96e3c 100644 --- a/src/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/src/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -73,9 +73,9 @@ namespace NzbDrone.Integration.Test.Client // cache control header gets reordered on net core var headers = response.Headers; ((string)headers.Single(c => c.Name == "Cache-Control").Value).Split(',').Select(x => x.Trim()) - .Should().BeEquivalentTo("no-store, must-revalidate, no-cache, max-age=0".Split(',').Select(x => x.Trim())); + .Should().BeEquivalentTo("no-store, no-cache".Split(',').Select(x => x.Trim())); headers.Single(c => c.Name == "Pragma").Value.Should().Be("no-cache"); - headers.Single(c => c.Name == "Expires").Value.Should().Be("0"); + headers.Single(c => c.Name == "Expires").Value.Should().Be("-1"); } } diff --git a/src/NzbDrone.Integration.Test/CorsFixture.cs b/src/NzbDrone.Integration.Test/CorsFixture.cs index 810c5083e..85d7f6f93 100644 --- a/src/NzbDrone.Integration.Test/CorsFixture.cs +++ b/src/NzbDrone.Integration.Test/CorsFixture.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Integration.Test private RestRequest BuildGet(string route = "artist") { var request = new RestRequest(route, Method.GET); + request.AddHeader("Origin", "http://a.different.domain"); request.AddHeader(AccessControlHeaders.RequestMethod, "POST"); return request; @@ -19,6 +20,8 @@ namespace NzbDrone.Integration.Test private RestRequest BuildOptions(string route = "artist") { var request = new RestRequest(route, Method.OPTIONS); + request.AddHeader("Origin", "http://a.different.domain"); + request.AddHeader(AccessControlHeaders.RequestMethod, "POST"); return request; }