From 59800d367503befd9cf8ac4b7cfdd63821dc4351 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 27 Jan 2025 16:27:42 -0800 Subject: [PATCH] v5 API docs --- .github/workflows/api_docs.yml | 8 +- CONTRIBUTING.md | 26 +- docs.sh | 11 +- src/NzbDrone.Host/Startup.cs | 33 ++ .../Qualities/QualityDefinitionResource.cs | 3 + src/Sonarr.Api.V3/openapi.json | 15 + src/Sonarr.Api.V5/openapi.json | 551 ++++++++++++++++++ .../VersionedApiControllerAttribute.cs | 2 + .../VersionedFeedControllerAttribute.cs | 8 + 9 files changed, 637 insertions(+), 20 deletions(-) create mode 100644 src/Sonarr.Api.V5/openapi.json diff --git a/.github/workflows/api_docs.yml b/.github/workflows/api_docs.yml index dfd8ce0e2..962ab1f7a 100644 --- a/.github/workflows/api_docs.yml +++ b/.github/workflows/api_docs.yml @@ -1,12 +1,12 @@ -name: 'API Docs' +name: "API Docs" on: workflow_dispatch: schedule: - - cron: '0 0 * * 1' + - cron: "0 0 * * 1" push: branches: - - develop + - v5-develop paths: - ".github/workflows/api_docs.yml" - "docs.sh" @@ -46,7 +46,7 @@ jobs: then git commit -am 'Automated API Docs update' -m "ignore-downstream" git push -f --set-upstream origin api-docs - curl -X POST -H "Authorization: Bearer ${{ secrets.OPENAPI_PAT }}" -H "Accept: application/vnd.github+json" https://api.github.com/repos/sonarr/sonarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}' + curl -X POST -H "Authorization: Bearer ${{ secrets.OPENAPI_PAT }}" -H "Accept: application/vnd.github+json" https://api.github.com/repos/sonarr/sonarr/pulls -d '{"head":"api-docs","base":"v5-develop","title":"Update API docs"}' else echo "No changes since last run" fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9eedd495b..8f00d3d2c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,32 +1,35 @@ -# How to Contribute # +# How to Contribute We're always looking for people to help make Sonarr even better, there are a number of ways to contribute. -## Documentation ## +## Documentation + Setup guides, [FAQ](https://wiki.servarr.com/sonarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/sonarr) the better. -## Development ## +## Development + +### Tools required -### Tools required ### -- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/). +- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/). - HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc) - [Git](https://git-scm.com/downloads) - [NodeJS](https://nodejs.org/en/download/) (Node 10.X.X or higher) - [Yarn](https://yarnpkg.com/) -### Getting started ### +### Getting started 1. Fork Sonarr -2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/get-started/quickstart/fork-a-repo) +2. Clone the repository into your development machine. [_info_](https://docs.github.com/en/get-started/quickstart/fork-a-repo) 3. Install the required Node Packages `yarn install` 4. Start webpack to monitor your dev environment for any frontend changes that need post processing using `yarn start` command. 5. Build the project in Visual Studio, Setting startup project to `Sonarr.Console` and framework to `x86` 6. Debug the project in Visual Studio 7. Open http://localhost:8989 -### Contributing Code ### +### Contributing Code + - If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Sonarr/Sonarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first) -- Rebase from Sonarr's `develop` branch, don't merge +- Rebase from Sonarr's `v5-develop` branch, don't merge - Make meaningful commits, or squash them - Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements - Reach out to us on our [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord](https://discord.gg/Ex7FmFK), or [IRC](https://web.libera.chat/?channels=#sonarr) if you have any questions @@ -35,8 +38,9 @@ Setup guides, [FAQ](https://wiki.servarr.com/sonarr/faq), the more information w - One feature/bug fix per pull request to keep things clean and easy to understand - Use 4 spaces instead of tabs, this should be the default for VS 2019 and WebStorm -### Pull Requesting ### -- Only make pull requests to develop (currently `develop`), never `main`, if you make a PR to master we'll comment on it and close it +### Pull Requesting + +- Only make pull requests to the default branch (currently `v5-develop`), never `main`, if you make a PR to main we'll comment on it and close it - You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability - We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it - Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed) diff --git a/docs.sh b/docs.sh index 0f3733f4e..47846fe99 100755 --- a/docs.sh +++ b/docs.sh @@ -3,13 +3,14 @@ set -e FRAMEWORK="net8.0" PLATFORM=$1 +ARCHITECTURE="${2:-'default value'}" if [ "$PLATFORM" = "Windows" ]; then - RUNTIME="win-x64" + RUNTIME="win-$ARCHITECTURE" elif [ "$PLATFORM" = "Linux" ]; then - RUNTIME="linux-x64" + RUNTIME="linux-$ARCHITECTURE" elif [ "$PLATFORM" = "Mac" ]; then - RUNTIME="osx-x64" + RUNTIME="osx-$ARCHITECTURE" else echo "Platform must be provided as first argument: Windows, Linux or Mac" exit 1 @@ -36,10 +37,10 @@ dotnet clean $slnFile -c Release dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids -dotnet new tool-manifest +# dotnet new tool-manifests dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli -dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 & +dotnet tool run swagger tofile --output ./src/Sonarr.Api.V5/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v5 & sleep 45 diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index 979e8605d..a3816d946 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using DryIoc; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -178,6 +180,37 @@ namespace NzbDrone.Host }); c.DescribeAllParametersInCamelCase(); + + // Generate docs based on the controller's API version + c.DocInclusionPredicate((docName, apiDesc) => + { + Type type = null; + + if (apiDesc.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) + { + type = controllerActionDescriptor.ControllerTypeInfo; + } + + if (type == null) + { + return false; + } + + var versions = new List(); + + versions.AddRange(type + .GetCustomAttributes(true) + .OfType() + .Select(attr => attr.Version)); + + versions.AddRange(type + .GetCustomAttributes(true) + .OfType() + .Select(attr => attr.Version)); + + // Return anything with no version or a matching version + return !versions.Any() || versions.Any(v => $"v{v}" == docName); + }); }); services diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs index 85f4ac57d..27f9c7107 100644 --- a/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs @@ -9,6 +9,7 @@ namespace Sonarr.Api.V3.Qualities { public Quality Quality { get; set; } public string Title { get; set; } + public int Weight { get; set; } public double? MinSize { get; set; } public double? MaxSize { get; set; } public double? PreferredSize { get; set; } @@ -28,6 +29,7 @@ namespace Sonarr.Api.V3.Qualities Id = model.Id, Quality = model.Quality, Title = model.Title, + Weight = model.Weight, MinSize = model.MinSize, MaxSize = model.MaxSize, PreferredSize = model.PreferredSize @@ -46,6 +48,7 @@ namespace Sonarr.Api.V3.Qualities Id = resource.Id, Quality = resource.Quality, Title = resource.Title, + Weight = resource.Weight, MinSize = resource.MinSize, MaxSize = resource.MaxSize, PreferredSize = resource.PreferredSize diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 6ac128ffd..fd6c92a8e 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -10778,6 +10778,21 @@ }, "allowed": { "type": "boolean" + }, + "minSize": { + "type": "number", + "format": "double", + "nullable": true + }, + "maxSize": { + "type": "number", + "format": "double", + "nullable": true + }, + "preferredSize": { + "type": "number", + "format": "double", + "nullable": true } }, "additionalProperties": false diff --git a/src/Sonarr.Api.V5/openapi.json b/src/Sonarr.Api.V5/openapi.json new file mode 100644 index 000000000..c62d2ce63 --- /dev/null +++ b/src/Sonarr.Api.V5/openapi.json @@ -0,0 +1,551 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Sonarr", + "description": "Sonarr API docs - The v5 API docs apply to Sonarr v5 only.", + "license": { + "name": "GPL-3.0", + "url": "https://github.com/Sonarr/Sonarr/blob/develop/LICENSE" + }, + "version": "5.0.0" + }, + "servers": [ + { + "url": "{protocol}://{hostpath}", + "variables": { + "protocol": { + "default": "http", + "enum": [ + "http", + "https" + ] + }, + "hostpath": { + "default": "localhost:8989" + } + } + } + ], + "paths": { + "/api/v5/series/lookup": { + "get": { + "tags": [ + "SeriesLookup" + ], + "parameters": [ + { + "name": "term", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesResource" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AddSeriesOptions": { + "type": "object", + "properties": { + "ignoreEpisodesWithFiles": { + "type": "boolean" + }, + "ignoreEpisodesWithoutFiles": { + "type": "boolean" + }, + "monitor": { + "$ref": "#/components/schemas/MonitorTypes" + }, + "searchForMissingEpisodes": { + "type": "boolean" + }, + "searchForCutoffUnmetEpisodes": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "AlternateTitleResource": { + "type": "object", + "properties": { + "title": { + "type": "string", + "nullable": true + }, + "seasonNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "sceneSeasonNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "sceneOrigin": { + "type": "string", + "nullable": true + }, + "comment": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "Language": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "MediaCover": { + "type": "object", + "properties": { + "coverType": { + "$ref": "#/components/schemas/MediaCoverTypes" + }, + "url": { + "type": "string", + "nullable": true + }, + "remoteUrl": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "MediaCoverTypes": { + "enum": [ + "unknown", + "poster", + "banner", + "fanart", + "screenshot", + "headshot", + "clearlogo" + ], + "type": "string" + }, + "MonitorTypes": { + "enum": [ + "unknown", + "all", + "future", + "missing", + "existing", + "firstSeason", + "lastSeason", + "latestSeason", + "pilot", + "recent", + "monitorSpecials", + "unmonitorSpecials", + "none", + "skip" + ], + "type": "string" + }, + "NewItemMonitorTypes": { + "enum": [ + "all", + "none" + ], + "type": "string" + }, + "Ratings": { + "type": "object", + "properties": { + "votes": { + "type": "integer", + "format": "int32" + }, + "value": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "SeasonResource": { + "type": "object", + "properties": { + "seasonNumber": { + "type": "integer", + "format": "int32" + }, + "monitored": { + "type": "boolean" + }, + "statistics": { + "$ref": "#/components/schemas/SeasonStatisticsResource" + }, + "images": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaCover" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "SeasonStatisticsResource": { + "type": "object", + "properties": { + "nextAiring": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "previousAiring": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "episodeFileCount": { + "type": "integer", + "format": "int32" + }, + "episodeCount": { + "type": "integer", + "format": "int32" + }, + "totalEpisodeCount": { + "type": "integer", + "format": "int32" + }, + "sizeOnDisk": { + "type": "integer", + "format": "int64" + }, + "releaseGroups": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "percentOfEpisodes": { + "type": "number", + "format": "double", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SeriesResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "alternateTitles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlternateTitleResource" + }, + "nullable": true + }, + "sortTitle": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/SeriesStatusType" + }, + "ended": { + "type": "boolean", + "readOnly": true + }, + "profileName": { + "type": "string", + "nullable": true + }, + "overview": { + "type": "string", + "nullable": true + }, + "nextAiring": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "previousAiring": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "network": { + "type": "string", + "nullable": true + }, + "airTime": { + "type": "string", + "nullable": true + }, + "images": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaCover" + }, + "nullable": true + }, + "originalLanguage": { + "$ref": "#/components/schemas/Language" + }, + "remotePoster": { + "type": "string", + "nullable": true + }, + "seasons": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeasonResource" + }, + "nullable": true + }, + "year": { + "type": "integer", + "format": "int32" + }, + "path": { + "type": "string", + "nullable": true + }, + "qualityProfileId": { + "type": "integer", + "format": "int32" + }, + "seasonFolder": { + "type": "boolean" + }, + "monitored": { + "type": "boolean" + }, + "monitorNewItems": { + "$ref": "#/components/schemas/NewItemMonitorTypes" + }, + "useSceneNumbering": { + "type": "boolean" + }, + "runtime": { + "type": "integer", + "format": "int32" + }, + "tvdbId": { + "type": "integer", + "format": "int32" + }, + "tvRageId": { + "type": "integer", + "format": "int32" + }, + "tvMazeId": { + "type": "integer", + "format": "int32" + }, + "tmdbId": { + "type": "integer", + "format": "int32" + }, + "firstAired": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "lastAired": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "seriesType": { + "$ref": "#/components/schemas/SeriesTypes" + }, + "cleanTitle": { + "type": "string", + "nullable": true + }, + "imdbId": { + "type": "string", + "nullable": true + }, + "titleSlug": { + "type": "string", + "nullable": true + }, + "rootFolderPath": { + "type": "string", + "nullable": true + }, + "folder": { + "type": "string", + "nullable": true + }, + "certification": { + "type": "string", + "nullable": true + }, + "genres": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "added": { + "type": "string", + "format": "date-time" + }, + "addOptions": { + "$ref": "#/components/schemas/AddSeriesOptions" + }, + "ratings": { + "$ref": "#/components/schemas/Ratings" + }, + "statistics": { + "$ref": "#/components/schemas/SeriesStatisticsResource" + }, + "episodesChanged": { + "type": "boolean", + "nullable": true + } + }, + "additionalProperties": false + }, + "SeriesStatisticsResource": { + "type": "object", + "properties": { + "seasonCount": { + "type": "integer", + "format": "int32" + }, + "episodeFileCount": { + "type": "integer", + "format": "int32" + }, + "episodeCount": { + "type": "integer", + "format": "int32" + }, + "totalEpisodeCount": { + "type": "integer", + "format": "int32" + }, + "sizeOnDisk": { + "type": "integer", + "format": "int64" + }, + "releaseGroups": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "percentOfEpisodes": { + "type": "number", + "format": "double", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SeriesStatusType": { + "enum": [ + "continuing", + "ended", + "upcoming", + "deleted" + ], + "type": "string" + }, + "SeriesTypes": { + "enum": [ + "standard", + "daily", + "anime" + ], + "type": "string" + } + }, + "securitySchemes": { + "X-Api-Key": { + "type": "apiKey", + "description": "Apikey passed as header", + "name": "X-Api-Key", + "in": "header" + }, + "apikey": { + "type": "apiKey", + "description": "Apikey passed as query parameter", + "name": "apikey", + "in": "query" + } + } + }, + "security": [ + { + "X-Api-Key": [ ] + }, + { + "apikey": [ ] + } + ] +} \ No newline at end of file diff --git a/src/Sonarr.Http/VersionedApiControllerAttribute.cs b/src/Sonarr.Http/VersionedApiControllerAttribute.cs index 39de670f4..9023a1d29 100644 --- a/src/Sonarr.Http/VersionedApiControllerAttribute.cs +++ b/src/Sonarr.Http/VersionedApiControllerAttribute.cs @@ -15,6 +15,7 @@ namespace Sonarr.Http Resource = resource; Template = $"api/v{version}/{resource}"; PolicyName = API_CORS_POLICY; + Version = version; } public string Resource { get; } @@ -22,6 +23,7 @@ namespace Sonarr.Http public int? Order => 2; public string Name { get; set; } public string PolicyName { get; set; } + public int Version { get; set; } } public class V3ApiControllerAttribute : VersionedApiControllerAttribute diff --git a/src/Sonarr.Http/VersionedFeedControllerAttribute.cs b/src/Sonarr.Http/VersionedFeedControllerAttribute.cs index 239cd09d9..0c40195a2 100644 --- a/src/Sonarr.Http/VersionedFeedControllerAttribute.cs +++ b/src/Sonarr.Http/VersionedFeedControllerAttribute.cs @@ -24,4 +24,12 @@ namespace Sonarr.Http { } } + + public class V5FeedControllerAttribute : VersionedFeedControllerAttribute + { + public V5FeedControllerAttribute(string resource = "[controller]") + : base(5, resource) + { + } + } }