From f04de80e14c1b485b76f6efb780a5510cf8ff1ca Mon Sep 17 00:00:00 2001 From: uttarayan21 Date: Mon, 13 Oct 2025 20:35:49 +0530 Subject: [PATCH] feat(yarr): Added download option --- README.md | 44 +- sonarr.yaml | 8526 ++++++++++++++++++++ src/tui.rs | 261 +- yarr-api/README.md | 45 + yarr-api/examples/download_after_search.rs | 189 + yarr-api/src/lib.rs | 344 + yarr-api/src/tests.rs | 336 + 7 files changed, 9733 insertions(+), 12 deletions(-) create mode 100644 sonarr.yaml create mode 100644 yarr-api/examples/download_after_search.rs create mode 100644 yarr-api/src/tests.rs diff --git a/README.md b/README.md index 1e4e855..5214ed7 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ This workspace contains two crates: - Browse series and episodes - Monitor download queue - View download history +- **Search for new series and download releases** - Interactive search with release selection and download - Interactive TUI interface with vim-like keybind support - Configurable UI preferences and keybind modes - In-app configuration editing for Sonarr connection and UI settings @@ -98,6 +99,25 @@ yarr yarr tui ``` +#### Search and Download Functionality + +The Search tab provides powerful functionality for finding and downloading new series: + +1. **Search for Series**: Press `/` to enter search mode, type a series name, and press Enter +2. **View Releases**: Navigate to a search result and press Enter to view available releases +3. **Download**: In the releases popup, select a release and press Enter to download it + +**Release Information Display**: +- Status indicators: ✅ Available, ❌ Rejected, ⛔ Not Allowed +- File size, quality rating, indexer name +- Seeders/peers count for torrent releases +- Full release title for identification + +**Download Selection**: +- Only releases marked as "Available" can be downloaded +- Choose based on quality, file size, and seed count +- Downloads are sent to your configured download client + ### Command Line Mode List all series: @@ -180,11 +200,12 @@ yarr completions powershell > yarr.ps1 - `q` - Quit - `↑/↓` or `j/k` - Navigate up/down -- `Enter` - Select/expand +- `Enter` - Select/expand item or show releases (in Search tab) - `Tab` - Switch between tabs - `d` - Toggle details - `r` - Refresh data - `/` - Search (in Search tab) +- `Esc` - Close popups or cancel search - `s` - Save configuration changes ### Vim Mode @@ -198,6 +219,8 @@ yarr completions powershell > yarr.ps1 - `u` - Refresh data (undo) - `/` - Search mode - `i` - Insert/input mode +- `Enter` - Select/expand item or show releases (in Search tab) +- `Esc` - Close popups or cancel operations - `s` - Save configuration changes ### Settings Tab @@ -210,6 +233,23 @@ Use the Settings tab to: Access the Settings tab by navigating to the last tab or pressing `Tab` repeatedly. +### Search Tab Usage + +The Search tab provides comprehensive series search and download functionality: + +1. **Enter Search Mode**: Press `/` to start typing a search query +2. **Search**: Type the series name and press Enter to search +3. **Browse Results**: Use ↑/↓ or j/k to navigate through search results +4. **View Releases**: Press Enter on a search result to open the releases popup +5. **Download**: In the releases popup, select a release and press Enter to download +6. **Close Popup**: Press Esc to close the releases popup + +**Release Popup Features**: +- Shows all available releases for the selected series +- Displays quality, size, indexer, and availability status +- Color-coded status indicators for easy identification +- Download progress feedback through status messages + ### In-App Configuration You can configure Sonarr connection settings directly within the application: @@ -261,7 +301,7 @@ async fn main() -> Result<()> { } ``` -See the [yarr-api README](yarr-api/README.md) for detailed API documentation and examples. +See the [yarr-api README](yarr-api/README.md) for detailed API documentation and examples, including the new download after search functionality. ## Development diff --git a/sonarr.yaml b/sonarr.yaml new file mode 100644 index 0000000..995e80a --- /dev/null +++ b/sonarr.yaml @@ -0,0 +1,8526 @@ +openapi: 3.1.1 +info: + title: Sonarr + description: Sonarr API docs - The v3 API docs apply to both v3 and v4 versions + of Sonarr. Some functionality may only be available in v4 of the Sonarr + application. + license: + name: GPL-3.0 + url: https://github.com/Sonarr/Sonarr/blob/develop/LICENSE + version: 3.0.0 +servers: + - url: "{protocol}://{hostpath}" + variables: + protocol: + default: http + enum: + - http + - https + hostpath: + default: localhost:8989 +paths: + /api: + get: + tags: + - ApiInfo + responses: + "200": + description: OK + /login: + post: + tags: + - Authentication + parameters: + - name: returnUrl + in: query + schema: + type: string + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + username: + type: string + password: + type: string + rememberMe: + type: string + encoding: + username: + style: form + password: + style: form + rememberMe: + style: form + responses: + "200": + description: OK + get: + tags: + - StaticResource + responses: + "200": + description: OK + /logout: + get: + tags: + - Authentication + responses: + "200": + description: OK + /api/v3/autotagging: + post: + tags: + - AutoTagging + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AutoTaggingResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/AutoTaggingResource" + application/json: + schema: + $ref: "#/components/schemas/AutoTaggingResource" + text/json: + schema: + $ref: "#/components/schemas/AutoTaggingResource" + get: + tags: + - AutoTagging + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/AutoTaggingResource" + /api/v3/autotagging/{id}: + put: + tags: + - AutoTagging + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AutoTaggingResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/AutoTaggingResource" + application/json: + schema: + $ref: "#/components/schemas/AutoTaggingResource" + text/json: + schema: + $ref: "#/components/schemas/AutoTaggingResource" + delete: + tags: + - AutoTagging + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - AutoTagging + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/AutoTaggingResource" + /api/v3/autotagging/schema: + get: + tags: + - AutoTagging + responses: + "200": + description: OK + /api/v3/system/backup: + get: + tags: + - Backup + responses: + "200": + description: OK + content: + text/plain: + schema: + type: array + items: + $ref: "#/components/schemas/BackupResource" + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/BackupResource" + text/json: + schema: + type: array + items: + $ref: "#/components/schemas/BackupResource" + /api/v3/system/backup/{id}: + delete: + tags: + - Backup + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + /api/v3/system/backup/restore/{id}: + post: + tags: + - Backup + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + /api/v3/system/backup/restore/upload: + post: + tags: + - Backup + responses: + "200": + description: OK + /api/v3/blocklist: + get: + tags: + - Blocklist + parameters: + - name: page + in: query + schema: + type: integer + format: int32 + default: 1 + - name: pageSize + in: query + schema: + type: integer + format: int32 + default: 10 + - name: sortKey + in: query + schema: + type: string + - name: sortDirection + in: query + schema: + $ref: "#/components/schemas/SortDirection" + - name: seriesIds + in: query + schema: + type: array + items: + type: integer + format: int32 + - name: protocols + in: query + schema: + type: array + items: + $ref: "#/components/schemas/DownloadProtocol" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/BlocklistResourcePagingResource" + /api/v3/blocklist/{id}: + delete: + tags: + - Blocklist + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + /api/v3/blocklist/bulk: + delete: + tags: + - Blocklist + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/BlocklistBulkResource" + text/json: + schema: + $ref: "#/components/schemas/BlocklistBulkResource" + application/*+json: + schema: + $ref: "#/components/schemas/BlocklistBulkResource" + responses: + "200": + description: OK + /api/v3/calendar: + get: + tags: + - Calendar + parameters: + - name: start + in: query + schema: + type: string + format: date-time + - name: end + in: query + schema: + type: string + format: date-time + - name: unmonitored + in: query + schema: + type: boolean + default: false + - name: includeSeries + in: query + schema: + type: boolean + default: false + - name: includeEpisodeFile + in: query + schema: + type: boolean + default: false + - name: includeEpisodeImages + in: query + schema: + type: boolean + default: false + - name: tags + in: query + schema: + type: string + default: "" + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EpisodeResource" + /api/v3/calendar/{id}: + get: + tags: + - Calendar + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeResource" + /feed/v3/calendar/sonarr.ics: + get: + tags: + - CalendarFeed + parameters: + - name: pastDays + in: query + schema: + type: integer + format: int32 + default: 7 + - name: futureDays + in: query + schema: + type: integer + format: int32 + default: 28 + - name: tags + in: query + schema: + type: string + default: "" + - name: unmonitored + in: query + schema: + type: boolean + default: false + - name: premieresOnly + in: query + schema: + type: boolean + default: false + - name: asAllDay + in: query + schema: + type: boolean + default: false + responses: + "200": + description: OK + /api/v3/command: + post: + tags: + - Command + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CommandResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/CommandResource" + get: + tags: + - Command + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CommandResource" + /api/v3/command/{id}: + delete: + tags: + - Command + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - Command + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/CommandResource" + /api/v3/customfilter: + get: + tags: + - CustomFilter + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CustomFilterResource" + post: + tags: + - CustomFilter + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CustomFilterResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/CustomFilterResource" + application/json: + schema: + $ref: "#/components/schemas/CustomFilterResource" + text/json: + schema: + $ref: "#/components/schemas/CustomFilterResource" + /api/v3/customfilter/{id}: + put: + tags: + - CustomFilter + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CustomFilterResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/CustomFilterResource" + application/json: + schema: + $ref: "#/components/schemas/CustomFilterResource" + text/json: + schema: + $ref: "#/components/schemas/CustomFilterResource" + delete: + tags: + - CustomFilter + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - CustomFilter + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/CustomFilterResource" + /api/v3/customformat: + get: + tags: + - CustomFormat + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CustomFormatResource" + post: + tags: + - CustomFormat + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CustomFormatResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/CustomFormatResource" + application/json: + schema: + $ref: "#/components/schemas/CustomFormatResource" + text/json: + schema: + $ref: "#/components/schemas/CustomFormatResource" + /api/v3/customformat/{id}: + put: + tags: + - CustomFormat + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CustomFormatResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/CustomFormatResource" + application/json: + schema: + $ref: "#/components/schemas/CustomFormatResource" + text/json: + schema: + $ref: "#/components/schemas/CustomFormatResource" + delete: + tags: + - CustomFormat + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - CustomFormat + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/CustomFormatResource" + /api/v3/customformat/bulk: + put: + tags: + - CustomFormat + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CustomFormatBulkResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/CustomFormatResource" + delete: + tags: + - CustomFormat + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CustomFormatBulkResource" + responses: + "200": + description: OK + /api/v3/customformat/schema: + get: + tags: + - CustomFormat + responses: + "200": + description: OK + /api/v3/wanted/cutoff: + get: + tags: + - Cutoff + parameters: + - name: page + in: query + schema: + type: integer + format: int32 + default: 1 + - name: pageSize + in: query + schema: + type: integer + format: int32 + default: 10 + - name: sortKey + in: query + schema: + type: string + - name: sortDirection + in: query + schema: + $ref: "#/components/schemas/SortDirection" + - name: includeSeries + in: query + schema: + type: boolean + default: false + - name: includeEpisodeFile + in: query + schema: + type: boolean + default: false + - name: includeImages + in: query + schema: + type: boolean + default: false + - name: monitored + in: query + schema: + type: boolean + default: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeResourcePagingResource" + /api/v3/wanted/cutoff/{id}: + get: + tags: + - Cutoff + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeResource" + /api/v3/delayprofile: + post: + tags: + - DelayProfile + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DelayProfileResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/DelayProfileResource" + application/json: + schema: + $ref: "#/components/schemas/DelayProfileResource" + text/json: + schema: + $ref: "#/components/schemas/DelayProfileResource" + get: + tags: + - DelayProfile + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DelayProfileResource" + /api/v3/delayprofile/{id}: + delete: + tags: + - DelayProfile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + put: + tags: + - DelayProfile + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DelayProfileResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/DelayProfileResource" + application/json: + schema: + $ref: "#/components/schemas/DelayProfileResource" + text/json: + schema: + $ref: "#/components/schemas/DelayProfileResource" + get: + tags: + - DelayProfile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/DelayProfileResource" + /api/v3/delayprofile/reorder/{id}: + put: + tags: + - DelayProfile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + - name: after + in: query + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + text/plain: + schema: + type: array + items: + $ref: "#/components/schemas/DelayProfileResource" + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DelayProfileResource" + text/json: + schema: + type: array + items: + $ref: "#/components/schemas/DelayProfileResource" + /api/v3/diskspace: + get: + tags: + - DiskSpace + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DiskSpaceResource" + /api/v3/downloadclient: + get: + tags: + - DownloadClient + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DownloadClientResource" + post: + tags: + - DownloadClient + parameters: + - name: forceSave + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientResource" + /api/v3/downloadclient/{id}: + put: + tags: + - DownloadClient + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + - name: forceSave + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientResource" + delete: + tags: + - DownloadClient + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - DownloadClient + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientResource" + /api/v3/downloadclient/bulk: + put: + tags: + - DownloadClient + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientBulkResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientResource" + delete: + tags: + - DownloadClient + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientBulkResource" + responses: + "200": + description: OK + /api/v3/downloadclient/schema: + get: + tags: + - DownloadClient + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DownloadClientResource" + /api/v3/downloadclient/test: + post: + tags: + - DownloadClient + parameters: + - name: forceTest + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientResource" + responses: + "200": + description: OK + /api/v3/downloadclient/testall: + post: + tags: + - DownloadClient + responses: + "200": + description: OK + /api/v3/downloadclient/action/{name}: + post: + tags: + - DownloadClient + parameters: + - name: name + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientResource" + responses: + "200": + description: OK + /api/v3/config/downloadclient: + get: + tags: + - DownloadClientConfig + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientConfigResource" + /api/v3/config/downloadclient/{id}: + put: + tags: + - DownloadClientConfig + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientConfigResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/DownloadClientConfigResource" + application/json: + schema: + $ref: "#/components/schemas/DownloadClientConfigResource" + text/json: + schema: + $ref: "#/components/schemas/DownloadClientConfigResource" + get: + tags: + - DownloadClientConfig + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadClientConfigResource" + /api/v3/episode: + get: + tags: + - Episode + parameters: + - name: seriesId + in: query + schema: + type: integer + format: int32 + - name: seasonNumber + in: query + schema: + type: integer + format: int32 + - name: episodeIds + in: query + schema: + type: array + items: + type: integer + format: int32 + - name: episodeFileId + in: query + schema: + type: integer + format: int32 + - name: includeSeries + in: query + schema: + type: boolean + default: false + - name: includeEpisodeFile + in: query + schema: + type: boolean + default: false + - name: includeImages + in: query + schema: + type: boolean + default: false + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EpisodeResource" + /api/v3/episode/{id}: + put: + tags: + - Episode + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/EpisodeResource" + application/json: + schema: + $ref: "#/components/schemas/EpisodeResource" + text/json: + schema: + $ref: "#/components/schemas/EpisodeResource" + get: + tags: + - Episode + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeResource" + /api/v3/episode/monitor: + put: + tags: + - Episode + parameters: + - name: includeImages + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodesMonitoredResource" + responses: + "200": + description: OK + /api/v3/episodefile: + get: + tags: + - EpisodeFile + parameters: + - name: seriesId + in: query + schema: + type: integer + format: int32 + - name: episodeFileIds + in: query + schema: + type: array + items: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EpisodeFileResource" + /api/v3/episodefile/{id}: + put: + tags: + - EpisodeFile + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeFileResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/EpisodeFileResource" + application/json: + schema: + $ref: "#/components/schemas/EpisodeFileResource" + text/json: + schema: + $ref: "#/components/schemas/EpisodeFileResource" + delete: + tags: + - EpisodeFile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - EpisodeFile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeFileResource" + /api/v3/episodefile/editor: + put: + tags: + - EpisodeFile + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeFileListResource" + responses: + "200": + description: OK + /api/v3/episodefile/bulk: + delete: + tags: + - EpisodeFile + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeFileListResource" + responses: + "200": + description: OK + put: + tags: + - EpisodeFile + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EpisodeFileResource" + responses: + "200": + description: OK + /api/v3/filesystem: + get: + tags: + - FileSystem + parameters: + - name: path + in: query + schema: + type: string + - name: includeFiles + in: query + schema: + type: boolean + default: false + - name: allowFoldersWithoutTrailingSlashes + in: query + schema: + type: boolean + default: false + responses: + "200": + description: OK + /api/v3/filesystem/type: + get: + tags: + - FileSystem + parameters: + - name: path + in: query + schema: + type: string + responses: + "200": + description: OK + /api/v3/filesystem/mediafiles: + get: + tags: + - FileSystem + parameters: + - name: path + in: query + schema: + type: string + responses: + "200": + description: OK + /api/v3/health: + get: + tags: + - Health + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/HealthResource" + /api/v3/history: + get: + tags: + - History + parameters: + - name: page + in: query + schema: + type: integer + format: int32 + default: 1 + - name: pageSize + in: query + schema: + type: integer + format: int32 + default: 10 + - name: sortKey + in: query + schema: + type: string + - name: sortDirection + in: query + schema: + $ref: "#/components/schemas/SortDirection" + - name: includeSeries + in: query + schema: + type: boolean + - name: includeEpisode + in: query + schema: + type: boolean + - name: eventType + in: query + schema: + type: array + items: + type: integer + format: int32 + - name: episodeId + in: query + schema: + type: integer + format: int32 + - name: downloadId + in: query + schema: + type: string + - name: seriesIds + in: query + schema: + type: array + items: + type: integer + format: int32 + - name: languages + in: query + schema: + type: array + items: + type: integer + format: int32 + - name: quality + in: query + schema: + type: array + items: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/HistoryResourcePagingResource" + /api/v3/history/since: + get: + tags: + - History + parameters: + - name: date + in: query + schema: + type: string + format: date-time + - name: eventType + in: query + schema: + $ref: "#/components/schemas/EpisodeHistoryEventType" + - name: includeSeries + in: query + schema: + type: boolean + default: false + - name: includeEpisode + in: query + schema: + type: boolean + default: false + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/HistoryResource" + /api/v3/history/series: + get: + tags: + - History + parameters: + - name: seriesId + in: query + schema: + type: integer + format: int32 + - name: seasonNumber + in: query + schema: + type: integer + format: int32 + - name: eventType + in: query + schema: + $ref: "#/components/schemas/EpisodeHistoryEventType" + - name: includeSeries + in: query + schema: + type: boolean + default: false + - name: includeEpisode + in: query + schema: + type: boolean + default: false + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/HistoryResource" + /api/v3/history/failed/{id}: + post: + tags: + - History + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + /api/v3/config/host: + get: + tags: + - HostConfig + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/HostConfigResource" + application/json: + schema: + $ref: "#/components/schemas/HostConfigResource" + text/json: + schema: + $ref: "#/components/schemas/HostConfigResource" + /api/v3/config/host/{id}: + put: + tags: + - HostConfig + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/HostConfigResource" + text/json: + schema: + $ref: "#/components/schemas/HostConfigResource" + application/*+json: + schema: + $ref: "#/components/schemas/HostConfigResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/HostConfigResource" + application/json: + schema: + $ref: "#/components/schemas/HostConfigResource" + text/json: + schema: + $ref: "#/components/schemas/HostConfigResource" + get: + tags: + - HostConfig + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/HostConfigResource" + /api/v3/importlist: + get: + tags: + - ImportList + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ImportListResource" + post: + tags: + - ImportList + parameters: + - name: forceSave + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListResource" + /api/v3/importlist/{id}: + put: + tags: + - ImportList + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + - name: forceSave + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListResource" + delete: + tags: + - ImportList + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - ImportList + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListResource" + /api/v3/importlist/bulk: + put: + tags: + - ImportList + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListBulkResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListResource" + delete: + tags: + - ImportList + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListBulkResource" + responses: + "200": + description: OK + /api/v3/importlist/schema: + get: + tags: + - ImportList + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ImportListResource" + /api/v3/importlist/test: + post: + tags: + - ImportList + parameters: + - name: forceTest + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListResource" + responses: + "200": + description: OK + /api/v3/importlist/testall: + post: + tags: + - ImportList + responses: + "200": + description: OK + /api/v3/importlist/action/{name}: + post: + tags: + - ImportList + parameters: + - name: name + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListResource" + responses: + "200": + description: OK + /api/v3/config/importlist: + get: + tags: + - ImportListConfig + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListConfigResource" + /api/v3/config/importlist/{id}: + put: + tags: + - ImportListConfig + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListConfigResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/ImportListConfigResource" + application/json: + schema: + $ref: "#/components/schemas/ImportListConfigResource" + text/json: + schema: + $ref: "#/components/schemas/ImportListConfigResource" + get: + tags: + - ImportListConfig + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListConfigResource" + /api/v3/importlistexclusion: + get: + tags: + - ImportListExclusion + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ImportListExclusionResource" + deprecated: true + post: + tags: + - ImportListExclusion + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListExclusionResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/ImportListExclusionResource" + application/json: + schema: + $ref: "#/components/schemas/ImportListExclusionResource" + text/json: + schema: + $ref: "#/components/schemas/ImportListExclusionResource" + /api/v3/importlistexclusion/paged: + get: + tags: + - ImportListExclusion + parameters: + - name: page + in: query + schema: + type: integer + format: int32 + default: 1 + - name: pageSize + in: query + schema: + type: integer + format: int32 + default: 10 + - name: sortKey + in: query + schema: + type: string + - name: sortDirection + in: query + schema: + $ref: "#/components/schemas/SortDirection" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListExclusionResourcePagingResource" + /api/v3/importlistexclusion/{id}: + put: + tags: + - ImportListExclusion + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListExclusionResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/ImportListExclusionResource" + application/json: + schema: + $ref: "#/components/schemas/ImportListExclusionResource" + text/json: + schema: + $ref: "#/components/schemas/ImportListExclusionResource" + delete: + tags: + - ImportListExclusion + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - ImportListExclusion + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListExclusionResource" + /api/v3/importlistexclusion/bulk: + delete: + tags: + - ImportListExclusion + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ImportListExclusionBulkResource" + text/json: + schema: + $ref: "#/components/schemas/ImportListExclusionBulkResource" + application/*+json: + schema: + $ref: "#/components/schemas/ImportListExclusionBulkResource" + responses: + "200": + description: OK + /api/v3/indexer: + get: + tags: + - Indexer + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/IndexerResource" + post: + tags: + - Indexer + parameters: + - name: forceSave + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerResource" + /api/v3/indexer/{id}: + put: + tags: + - Indexer + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + - name: forceSave + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerResource" + delete: + tags: + - Indexer + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - Indexer + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerResource" + /api/v3/indexer/bulk: + put: + tags: + - Indexer + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerBulkResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerResource" + delete: + tags: + - Indexer + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerBulkResource" + responses: + "200": + description: OK + /api/v3/indexer/schema: + get: + tags: + - Indexer + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/IndexerResource" + /api/v3/indexer/test: + post: + tags: + - Indexer + parameters: + - name: forceTest + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerResource" + responses: + "200": + description: OK + /api/v3/indexer/testall: + post: + tags: + - Indexer + responses: + "200": + description: OK + /api/v3/indexer/action/{name}: + post: + tags: + - Indexer + parameters: + - name: name + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerResource" + responses: + "200": + description: OK + /api/v3/config/indexer: + get: + tags: + - IndexerConfig + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerConfigResource" + /api/v3/config/indexer/{id}: + put: + tags: + - IndexerConfig + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerConfigResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/IndexerConfigResource" + application/json: + schema: + $ref: "#/components/schemas/IndexerConfigResource" + text/json: + schema: + $ref: "#/components/schemas/IndexerConfigResource" + get: + tags: + - IndexerConfig + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerConfigResource" + /api/v3/indexerflag: + get: + tags: + - IndexerFlag + responses: + "200": + description: OK + content: + text/plain: + schema: + type: array + items: + $ref: "#/components/schemas/IndexerFlagResource" + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/IndexerFlagResource" + text/json: + schema: + type: array + items: + $ref: "#/components/schemas/IndexerFlagResource" + /api/v3/language: + get: + tags: + - Language + responses: + "200": + description: OK + content: + text/plain: + schema: + type: array + items: + $ref: "#/components/schemas/LanguageResource" + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/LanguageResource" + text/json: + schema: + type: array + items: + $ref: "#/components/schemas/LanguageResource" + /api/v3/language/{id}: + get: + tags: + - Language + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/LanguageResource" + /api/v3/languageprofile: + post: + tags: + - LanguageProfile + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/LanguageProfileResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/LanguageProfileResource" + deprecated: true + get: + tags: + - LanguageProfile + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/LanguageProfileResource" + deprecated: true + /api/v3/languageprofile/{id}: + delete: + tags: + - LanguageProfile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + deprecated: true + put: + tags: + - LanguageProfile + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/LanguageProfileResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/LanguageProfileResource" + deprecated: true + get: + tags: + - LanguageProfile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/LanguageProfileResource" + /api/v3/languageprofile/schema: + get: + tags: + - LanguageProfileSchema + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/LanguageProfileResource" + deprecated: true + /api/v3/localization: + get: + tags: + - Localization + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/LocalizationResource" + /api/v3/localization/language: + get: + tags: + - Localization + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/LocalizationLanguageResource" + /api/v3/localization/{id}: + get: + tags: + - Localization + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/LocalizationResource" + /api/v3/log: + get: + tags: + - Log + parameters: + - name: page + in: query + schema: + type: integer + format: int32 + default: 1 + - name: pageSize + in: query + schema: + type: integer + format: int32 + default: 10 + - name: sortKey + in: query + schema: + type: string + - name: sortDirection + in: query + schema: + $ref: "#/components/schemas/SortDirection" + - name: level + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/LogResourcePagingResource" + /api/v3/log/file: + get: + tags: + - LogFile + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/LogFileResource" + /api/v3/log/file/{filename}: + get: + tags: + - LogFile + parameters: + - name: filename + in: path + required: true + schema: + pattern: "[-.a-zA-Z0-9]+?\\.txt" + type: string + responses: + "200": + description: OK + /api/v3/manualimport: + get: + tags: + - ManualImport + parameters: + - name: folder + in: query + schema: + type: string + - name: downloadId + in: query + schema: + type: string + - name: seriesId + in: query + schema: + type: integer + format: int32 + - name: seasonNumber + in: query + schema: + type: integer + format: int32 + - name: filterExistingFiles + in: query + schema: + type: boolean + default: true + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ManualImportResource" + post: + tags: + - ManualImport + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ManualImportReprocessResource" + responses: + "200": + description: OK + /api/v3/mediacover/{seriesId}/{filename}: + get: + tags: + - MediaCover + parameters: + - name: seriesId + in: path + required: true + schema: + type: integer + format: int32 + - name: filename + in: path + required: true + schema: + pattern: (.+)\.(jpg|png|gif) + type: string + responses: + "200": + description: OK + /api/v3/config/mediamanagement: + get: + tags: + - MediaManagementConfig + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/MediaManagementConfigResource" + /api/v3/config/mediamanagement/{id}: + put: + tags: + - MediaManagementConfig + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MediaManagementConfigResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/MediaManagementConfigResource" + application/json: + schema: + $ref: "#/components/schemas/MediaManagementConfigResource" + text/json: + schema: + $ref: "#/components/schemas/MediaManagementConfigResource" + get: + tags: + - MediaManagementConfig + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/MediaManagementConfigResource" + /api/v3/metadata: + get: + tags: + - Metadata + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/MetadataResource" + post: + tags: + - Metadata + parameters: + - name: forceSave + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataResource" + /api/v3/metadata/{id}: + put: + tags: + - Metadata + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + - name: forceSave + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataResource" + delete: + tags: + - Metadata + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - Metadata + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataResource" + /api/v3/metadata/schema: + get: + tags: + - Metadata + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/MetadataResource" + /api/v3/metadata/test: + post: + tags: + - Metadata + parameters: + - name: forceTest + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataResource" + responses: + "200": + description: OK + /api/v3/metadata/testall: + post: + tags: + - Metadata + responses: + "200": + description: OK + /api/v3/metadata/action/{name}: + post: + tags: + - Metadata + parameters: + - name: name + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataResource" + responses: + "200": + description: OK + /api/v3/wanted/missing: + get: + tags: + - Missing + parameters: + - name: page + in: query + schema: + type: integer + format: int32 + default: 1 + - name: pageSize + in: query + schema: + type: integer + format: int32 + default: 10 + - name: sortKey + in: query + schema: + type: string + - name: sortDirection + in: query + schema: + $ref: "#/components/schemas/SortDirection" + - name: includeSeries + in: query + schema: + type: boolean + default: false + - name: includeImages + in: query + schema: + type: boolean + default: false + - name: monitored + in: query + schema: + type: boolean + default: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeResourcePagingResource" + /api/v3/wanted/missing/{id}: + get: + tags: + - Missing + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EpisodeResource" + /api/v3/config/naming: + get: + tags: + - NamingConfig + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/NamingConfigResource" + application/json: + schema: + $ref: "#/components/schemas/NamingConfigResource" + text/json: + schema: + $ref: "#/components/schemas/NamingConfigResource" + /api/v3/config/naming/{id}: + put: + tags: + - NamingConfig + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NamingConfigResource" + text/json: + schema: + $ref: "#/components/schemas/NamingConfigResource" + application/*+json: + schema: + $ref: "#/components/schemas/NamingConfigResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/NamingConfigResource" + application/json: + schema: + $ref: "#/components/schemas/NamingConfigResource" + text/json: + schema: + $ref: "#/components/schemas/NamingConfigResource" + get: + tags: + - NamingConfig + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/NamingConfigResource" + /api/v3/config/naming/examples: + get: + tags: + - NamingConfig + parameters: + - name: renameEpisodes + in: query + schema: + type: boolean + - name: replaceIllegalCharacters + in: query + schema: + type: boolean + - name: colonReplacementFormat + in: query + schema: + type: integer + format: int32 + - name: customColonReplacementFormat + in: query + schema: + type: string + - name: multiEpisodeStyle + in: query + schema: + type: integer + format: int32 + - name: standardEpisodeFormat + in: query + schema: + type: string + - name: dailyEpisodeFormat + in: query + schema: + type: string + - name: animeEpisodeFormat + in: query + schema: + type: string + - name: seriesFolderFormat + in: query + schema: + type: string + - name: seasonFolderFormat + in: query + schema: + type: string + - name: specialsFolderFormat + in: query + schema: + type: string + - name: id + in: query + schema: + type: integer + format: int32 + - name: resourceName + in: query + schema: + type: string + responses: + "200": + description: OK + /api/v3/notification: + get: + tags: + - Notification + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/NotificationResource" + post: + tags: + - Notification + parameters: + - name: forceSave + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NotificationResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/NotificationResource" + /api/v3/notification/{id}: + put: + tags: + - Notification + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + - name: forceSave + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NotificationResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/NotificationResource" + delete: + tags: + - Notification + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - Notification + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/NotificationResource" + /api/v3/notification/schema: + get: + tags: + - Notification + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/NotificationResource" + /api/v3/notification/test: + post: + tags: + - Notification + parameters: + - name: forceTest + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NotificationResource" + responses: + "200": + description: OK + /api/v3/notification/testall: + post: + tags: + - Notification + responses: + "200": + description: OK + /api/v3/notification/action/{name}: + post: + tags: + - Notification + parameters: + - name: name + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NotificationResource" + responses: + "200": + description: OK + /api/v3/parse: + get: + tags: + - Parse + parameters: + - name: title + in: query + schema: + type: string + - name: path + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ParseResource" + /ping: + get: + tags: + - Ping + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/PingResource" + head: + tags: + - Ping + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/PingResource" + /api/v3/qualitydefinition/{id}: + put: + tags: + - QualityDefinition + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/QualityDefinitionResource" + text/json: + schema: + $ref: "#/components/schemas/QualityDefinitionResource" + application/*+json: + schema: + $ref: "#/components/schemas/QualityDefinitionResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/QualityDefinitionResource" + application/json: + schema: + $ref: "#/components/schemas/QualityDefinitionResource" + text/json: + schema: + $ref: "#/components/schemas/QualityDefinitionResource" + get: + tags: + - QualityDefinition + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/QualityDefinitionResource" + /api/v3/qualitydefinition: + get: + tags: + - QualityDefinition + responses: + "200": + description: OK + content: + text/plain: + schema: + type: array + items: + $ref: "#/components/schemas/QualityDefinitionResource" + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/QualityDefinitionResource" + text/json: + schema: + type: array + items: + $ref: "#/components/schemas/QualityDefinitionResource" + /api/v3/qualitydefinition/update: + put: + tags: + - QualityDefinition + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/QualityDefinitionResource" + text/json: + schema: + type: array + items: + $ref: "#/components/schemas/QualityDefinitionResource" + application/*+json: + schema: + type: array + items: + $ref: "#/components/schemas/QualityDefinitionResource" + responses: + "200": + description: OK + /api/v3/qualitydefinition/limits: + get: + tags: + - QualityDefinition + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/QualityDefinitionLimitsResource" + application/json: + schema: + $ref: "#/components/schemas/QualityDefinitionLimitsResource" + text/json: + schema: + $ref: "#/components/schemas/QualityDefinitionLimitsResource" + /api/v3/qualityprofile: + post: + tags: + - QualityProfile + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/QualityProfileResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/QualityProfileResource" + application/json: + schema: + $ref: "#/components/schemas/QualityProfileResource" + text/json: + schema: + $ref: "#/components/schemas/QualityProfileResource" + get: + tags: + - QualityProfile + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/QualityProfileResource" + /api/v3/qualityprofile/{id}: + delete: + tags: + - QualityProfile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + put: + tags: + - QualityProfile + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/QualityProfileResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/QualityProfileResource" + application/json: + schema: + $ref: "#/components/schemas/QualityProfileResource" + text/json: + schema: + $ref: "#/components/schemas/QualityProfileResource" + get: + tags: + - QualityProfile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/QualityProfileResource" + /api/v3/qualityprofile/schema: + get: + tags: + - QualityProfileSchema + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/QualityProfileResource" + application/json: + schema: + $ref: "#/components/schemas/QualityProfileResource" + text/json: + schema: + $ref: "#/components/schemas/QualityProfileResource" + /api/v3/queue/{id}: + delete: + tags: + - Queue + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + - name: removeFromClient + in: query + schema: + type: boolean + default: true + - name: blocklist + in: query + schema: + type: boolean + default: false + - name: skipRedownload + in: query + schema: + type: boolean + default: false + - name: changeCategory + in: query + schema: + type: boolean + default: false + responses: + "200": + description: OK + /api/v3/queue/bulk: + delete: + tags: + - Queue + parameters: + - name: removeFromClient + in: query + schema: + type: boolean + default: true + - name: blocklist + in: query + schema: + type: boolean + default: false + - name: skipRedownload + in: query + schema: + type: boolean + default: false + - name: changeCategory + in: query + schema: + type: boolean + default: false + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/QueueBulkResource" + text/json: + schema: + $ref: "#/components/schemas/QueueBulkResource" + application/*+json: + schema: + $ref: "#/components/schemas/QueueBulkResource" + responses: + "200": + description: OK + /api/v3/queue: + get: + tags: + - Queue + parameters: + - name: page + in: query + schema: + type: integer + format: int32 + default: 1 + - name: pageSize + in: query + schema: + type: integer + format: int32 + default: 10 + - name: sortKey + in: query + schema: + type: string + - name: sortDirection + in: query + schema: + $ref: "#/components/schemas/SortDirection" + - name: includeUnknownSeriesItems + in: query + schema: + type: boolean + default: false + - name: includeSeries + in: query + schema: + type: boolean + default: false + - name: includeEpisode + in: query + schema: + type: boolean + default: false + - name: seriesIds + in: query + schema: + type: array + items: + type: integer + format: int32 + - name: protocol + in: query + schema: + $ref: "#/components/schemas/DownloadProtocol" + - name: languages + in: query + schema: + type: array + items: + type: integer + format: int32 + - name: quality + in: query + schema: + type: array + items: + type: integer + format: int32 + - name: status + in: query + schema: + type: array + items: + $ref: "#/components/schemas/QueueStatus" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/QueueResourcePagingResource" + /api/v3/queue/grab/{id}: + post: + tags: + - QueueAction + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + /api/v3/queue/grab/bulk: + post: + tags: + - QueueAction + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/QueueBulkResource" + responses: + "200": + description: OK + /api/v3/queue/details: + get: + tags: + - QueueDetails + parameters: + - name: seriesId + in: query + schema: + type: integer + format: int32 + - name: episodeIds + in: query + schema: + type: array + items: + type: integer + format: int32 + - name: includeSeries + in: query + schema: + type: boolean + default: false + - name: includeEpisode + in: query + schema: + type: boolean + default: false + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/QueueResource" + /api/v3/queue/status: + get: + tags: + - QueueStatus + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/QueueStatusResource" + /api/v3/release: + post: + tags: + - Release + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ReleaseResource" + responses: + "200": + description: OK + get: + tags: + - Release + parameters: + - name: seriesId + in: query + schema: + type: integer + format: int32 + - name: episodeId + in: query + schema: + type: integer + format: int32 + - name: seasonNumber + in: query + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ReleaseResource" + /api/v3/releaseprofile: + post: + tags: + - ReleaseProfile + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + text/json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + application/*+json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + application/json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + text/json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + get: + tags: + - ReleaseProfile + responses: + "200": + description: OK + content: + text/plain: + schema: + type: array + items: + $ref: "#/components/schemas/ReleaseProfileResource" + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ReleaseProfileResource" + text/json: + schema: + type: array + items: + $ref: "#/components/schemas/ReleaseProfileResource" + /api/v3/releaseprofile/{id}: + delete: + tags: + - ReleaseProfile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + put: + tags: + - ReleaseProfile + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + text/json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + application/*+json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + application/json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + text/json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + get: + tags: + - ReleaseProfile + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ReleaseProfileResource" + /api/v3/release/push: + post: + tags: + - ReleasePush + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ReleaseResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + type: array + items: + $ref: "#/components/schemas/ReleaseResource" + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ReleaseResource" + text/json: + schema: + type: array + items: + $ref: "#/components/schemas/ReleaseResource" + /api/v3/remotepathmapping: + post: + tags: + - RemotePathMapping + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + application/json: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + text/json: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + get: + tags: + - RemotePathMapping + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RemotePathMappingResource" + /api/v3/remotepathmapping/{id}: + delete: + tags: + - RemotePathMapping + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + put: + tags: + - RemotePathMapping + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + text/json: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + application/*+json: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + application/json: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + text/json: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + get: + tags: + - RemotePathMapping + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/RemotePathMappingResource" + /api/v3/rename: + get: + tags: + - RenameEpisode + parameters: + - name: seriesId + in: query + schema: + type: integer + format: int32 + - name: seasonNumber + in: query + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RenameEpisodeResource" + /api/v3/rootfolder: + post: + tags: + - RootFolder + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RootFolderResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/RootFolderResource" + application/json: + schema: + $ref: "#/components/schemas/RootFolderResource" + text/json: + schema: + $ref: "#/components/schemas/RootFolderResource" + get: + tags: + - RootFolder + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RootFolderResource" + /api/v3/rootfolder/{id}: + delete: + tags: + - RootFolder + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - RootFolder + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/RootFolderResource" + /api/v3/seasonpass: + post: + tags: + - SeasonPass + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SeasonPassResource" + responses: + "200": + description: OK + /api/v3/series: + get: + tags: + - Series + parameters: + - name: tvdbId + in: query + schema: + type: integer + format: int32 + - name: includeSeasonImages + in: query + schema: + type: boolean + default: false + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SeriesResource" + post: + tags: + - Series + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SeriesResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SeriesResource" + /api/v3/series/{id}: + get: + tags: + - Series + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + - name: includeSeasonImages + in: query + schema: + type: boolean + default: false + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SeriesResource" + put: + tags: + - Series + parameters: + - name: moveFiles + in: query + schema: + type: boolean + default: false + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SeriesResource" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SeriesResource" + delete: + tags: + - Series + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + - name: deleteFiles + in: query + schema: + type: boolean + default: false + - name: addImportListExclusion + in: query + schema: + type: boolean + default: false + responses: + "200": + description: OK + /api/v3/series/editor: + put: + tags: + - SeriesEditor + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SeriesEditorResource" + text/json: + schema: + $ref: "#/components/schemas/SeriesEditorResource" + application/*+json: + schema: + $ref: "#/components/schemas/SeriesEditorResource" + responses: + "200": + description: OK + delete: + tags: + - SeriesEditor + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SeriesEditorResource" + text/json: + schema: + $ref: "#/components/schemas/SeriesEditorResource" + application/*+json: + schema: + $ref: "#/components/schemas/SeriesEditorResource" + responses: + "200": + description: OK + /api/v3/series/{id}/folder: + get: + tags: + - SeriesFolder + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + /api/v3/series/import: + post: + tags: + - SeriesImport + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SeriesResource" + text/json: + schema: + type: array + items: + $ref: "#/components/schemas/SeriesResource" + application/*+json: + schema: + type: array + items: + $ref: "#/components/schemas/SeriesResource" + responses: + "200": + description: OK + /api/v3/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" + /content/{path}: + get: + tags: + - StaticResource + parameters: + - name: path + in: path + required: true + schema: + pattern: ^(?!api/).* + type: string + responses: + "200": + description: OK + /: + get: + tags: + - StaticResource + parameters: + - name: path + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + /{path}: + get: + tags: + - StaticResource + parameters: + - name: path + in: path + required: true + schema: + pattern: ^(?!(api|feed)/).* + type: string + responses: + "200": + description: OK + /api/v3/system/status: + get: + tags: + - System + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SystemResource" + /api/v3/system/routes: + get: + tags: + - System + responses: + "200": + description: OK + /api/v3/system/routes/duplicate: + get: + tags: + - System + responses: + "200": + description: OK + /api/v3/system/shutdown: + post: + tags: + - System + responses: + "200": + description: OK + /api/v3/system/restart: + post: + tags: + - System + responses: + "200": + description: OK + /api/v3/tag: + get: + tags: + - Tag + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TagResource" + post: + tags: + - Tag + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TagResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/TagResource" + application/json: + schema: + $ref: "#/components/schemas/TagResource" + text/json: + schema: + $ref: "#/components/schemas/TagResource" + /api/v3/tag/{id}: + put: + tags: + - Tag + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TagResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/TagResource" + application/json: + schema: + $ref: "#/components/schemas/TagResource" + text/json: + schema: + $ref: "#/components/schemas/TagResource" + delete: + tags: + - Tag + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + get: + tags: + - Tag + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/TagResource" + /api/v3/tag/detail: + get: + tags: + - TagDetails + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TagDetailsResource" + /api/v3/tag/detail/{id}: + get: + tags: + - TagDetails + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/TagDetailsResource" + /api/v3/system/task: + get: + tags: + - Task + responses: + "200": + description: OK + content: + text/plain: + schema: + type: array + items: + $ref: "#/components/schemas/TaskResource" + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TaskResource" + text/json: + schema: + type: array + items: + $ref: "#/components/schemas/TaskResource" + /api/v3/system/task/{id}: + get: + tags: + - Task + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/TaskResource" + /api/v3/config/ui/{id}: + put: + tags: + - UiConfig + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UiConfigResource" + responses: + "200": + description: OK + content: + text/plain: + schema: + $ref: "#/components/schemas/UiConfigResource" + application/json: + schema: + $ref: "#/components/schemas/UiConfigResource" + text/json: + schema: + $ref: "#/components/schemas/UiConfigResource" + get: + tags: + - UiConfig + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UiConfigResource" + /api/v3/config/ui: + get: + tags: + - UiConfig + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UiConfigResource" + /api/v3/update: + get: + tags: + - Update + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/UpdateResource" + /api/v3/log/file/update: + get: + tags: + - UpdateLogFile + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/LogFileResource" + /api/v3/log/file/update/{filename}: + get: + tags: + - UpdateLogFile + parameters: + - name: filename + in: path + required: true + schema: + pattern: "[-.a-zA-Z0-9]+?\\.txt" + type: string + responses: + "200": + description: OK +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 + - "null" + seasonNumber: + type: + - integer + - "null" + format: int32 + sceneSeasonNumber: + type: + - integer + - "null" + format: int32 + sceneOrigin: + type: + - string + - "null" + comment: + type: + - string + - "null" + additionalProperties: false + ApplyTags: + enum: + - add + - remove + - replace + type: string + AuthenticationRequiredType: + enum: + - enabled + - disabledForLocalAddresses + type: string + AuthenticationType: + enum: + - none + - basic + - forms + - external + type: string + AutoTaggingResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + removeTagsAutomatically: + type: boolean + tags: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + specifications: + type: + - array + - "null" + items: + $ref: "#/components/schemas/AutoTaggingSpecificationSchema" + additionalProperties: false + AutoTaggingSpecificationSchema: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + implementation: + type: + - string + - "null" + implementationName: + type: + - string + - "null" + negate: + type: boolean + required: + type: boolean + fields: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Field" + additionalProperties: false + BackupResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + path: + type: + - string + - "null" + type: + $ref: "#/components/schemas/BackupType" + size: + type: integer + format: int64 + time: + type: string + format: date-time + additionalProperties: false + BackupType: + enum: + - scheduled + - manual + - update + type: string + BlocklistBulkResource: + type: object + properties: + ids: + type: + - array + - "null" + items: + type: integer + format: int32 + additionalProperties: false + BlocklistResource: + type: object + properties: + id: + type: integer + format: int32 + seriesId: + type: integer + format: int32 + episodeIds: + type: + - array + - "null" + items: + type: integer + format: int32 + sourceTitle: + type: + - string + - "null" + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Language" + quality: + $ref: "#/components/schemas/QualityModel" + customFormats: + type: + - array + - "null" + items: + $ref: "#/components/schemas/CustomFormatResource" + date: + type: string + format: date-time + protocol: + $ref: "#/components/schemas/DownloadProtocol" + indexer: + type: + - string + - "null" + message: + type: + - string + - "null" + series: + $ref: "#/components/schemas/SeriesResource" + additionalProperties: false + BlocklistResourcePagingResource: + type: object + properties: + page: + type: integer + format: int32 + pageSize: + type: integer + format: int32 + sortKey: + type: + - string + - "null" + sortDirection: + $ref: "#/components/schemas/SortDirection" + totalRecords: + type: integer + format: int32 + records: + type: + - array + - "null" + items: + $ref: "#/components/schemas/BlocklistResource" + additionalProperties: false + CertificateValidationType: + enum: + - enabled + - disabledForLocalAddresses + - disabled + type: string + Command: + type: object + properties: + sendUpdatesToClient: + type: boolean + updateScheduledTask: + type: boolean + readOnly: true + completionMessage: + type: + - string + - "null" + readOnly: true + requiresDiskAccess: + type: boolean + readOnly: true + isExclusive: + type: boolean + readOnly: true + isLongRunning: + type: boolean + readOnly: true + name: + type: + - string + - "null" + readOnly: true + lastExecutionTime: + type: + - string + - "null" + format: date-time + lastStartTime: + type: + - string + - "null" + format: date-time + trigger: + $ref: "#/components/schemas/CommandTrigger" + suppressMessages: + type: boolean + clientUserAgent: + type: + - string + - "null" + additionalProperties: false + CommandPriority: + enum: + - normal + - high + - low + type: string + CommandResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + commandName: + type: + - string + - "null" + message: + type: + - string + - "null" + body: + $ref: "#/components/schemas/Command" + priority: + $ref: "#/components/schemas/CommandPriority" + status: + $ref: "#/components/schemas/CommandStatus" + result: + $ref: "#/components/schemas/CommandResult" + queued: + type: string + format: date-time + started: + type: + - string + - "null" + format: date-time + ended: + type: + - string + - "null" + format: date-time + duration: + type: + - string + - "null" + format: date-span + exception: + type: + - string + - "null" + trigger: + $ref: "#/components/schemas/CommandTrigger" + clientUserAgent: + type: + - string + - "null" + stateChangeTime: + type: + - string + - "null" + format: date-time + sendUpdatesToClient: + type: boolean + updateScheduledTask: + type: boolean + lastExecutionTime: + type: + - string + - "null" + format: date-time + additionalProperties: false + CommandResult: + enum: + - unknown + - successful + - unsuccessful + type: string + CommandStatus: + enum: + - queued + - started + - completed + - failed + - aborted + - cancelled + - orphaned + type: string + CommandTrigger: + enum: + - unspecified + - manual + - scheduled + type: string + CustomFilterResource: + type: object + properties: + id: + type: integer + format: int32 + type: + type: + - string + - "null" + label: + type: + - string + - "null" + filters: + type: + - array + - "null" + items: + type: object + additionalProperties: {} + additionalProperties: false + CustomFormatBulkResource: + type: object + properties: + ids: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + includeCustomFormatWhenRenaming: + type: + - boolean + - "null" + additionalProperties: false + CustomFormatResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + includeCustomFormatWhenRenaming: + type: + - boolean + - "null" + specifications: + type: + - array + - "null" + items: + $ref: "#/components/schemas/CustomFormatSpecificationSchema" + additionalProperties: false + CustomFormatSpecificationSchema: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + implementation: + type: + - string + - "null" + implementationName: + type: + - string + - "null" + infoLink: + type: + - string + - "null" + negate: + type: boolean + required: + type: boolean + fields: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Field" + presets: + type: + - array + - "null" + items: + $ref: "#/components/schemas/CustomFormatSpecificationSchema" + additionalProperties: false + DatabaseType: + enum: + - sqLite + - postgreSQL + type: string + DelayProfileResource: + type: object + properties: + id: + type: integer + format: int32 + enableUsenet: + type: boolean + enableTorrent: + type: boolean + preferredProtocol: + $ref: "#/components/schemas/DownloadProtocol" + usenetDelay: + type: integer + format: int32 + torrentDelay: + type: integer + format: int32 + bypassIfHighestQuality: + type: boolean + bypassIfAboveCustomFormatScore: + type: boolean + minimumCustomFormatScore: + type: integer + format: int32 + order: + type: integer + format: int32 + tags: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + additionalProperties: false + DiskSpaceResource: + type: object + properties: + id: + type: integer + format: int32 + path: + type: + - string + - "null" + label: + type: + - string + - "null" + freeSpace: + type: integer + format: int64 + totalSpace: + type: integer + format: int64 + additionalProperties: false + DownloadClientBulkResource: + type: object + properties: + ids: + type: + - array + - "null" + items: + type: integer + format: int32 + tags: + type: + - array + - "null" + items: + type: integer + format: int32 + applyTags: + $ref: "#/components/schemas/ApplyTags" + enable: + type: + - boolean + - "null" + priority: + type: + - integer + - "null" + format: int32 + removeCompletedDownloads: + type: + - boolean + - "null" + removeFailedDownloads: + type: + - boolean + - "null" + additionalProperties: false + DownloadClientConfigResource: + type: object + properties: + id: + type: integer + format: int32 + downloadClientWorkingFolders: + type: + - string + - "null" + enableCompletedDownloadHandling: + type: boolean + autoRedownloadFailed: + type: boolean + autoRedownloadFailedFromInteractiveSearch: + type: boolean + additionalProperties: false + DownloadClientResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + fields: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Field" + implementationName: + type: + - string + - "null" + implementation: + type: + - string + - "null" + configContract: + type: + - string + - "null" + infoLink: + type: + - string + - "null" + message: + $ref: "#/components/schemas/ProviderMessage" + tags: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + presets: + type: + - array + - "null" + items: + $ref: "#/components/schemas/DownloadClientResource" + enable: + type: boolean + protocol: + $ref: "#/components/schemas/DownloadProtocol" + priority: + type: integer + format: int32 + removeCompletedDownloads: + type: boolean + removeFailedDownloads: + type: boolean + additionalProperties: false + DownloadProtocol: + enum: + - unknown + - usenet + - torrent + type: string + EpisodeFileListResource: + type: object + properties: + episodeFileIds: + type: + - array + - "null" + items: + type: integer + format: int32 + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Language" + quality: + $ref: "#/components/schemas/QualityModel" + sceneName: + type: + - string + - "null" + releaseGroup: + type: + - string + - "null" + additionalProperties: false + EpisodeFileResource: + type: object + properties: + id: + type: integer + format: int32 + seriesId: + type: integer + format: int32 + seasonNumber: + type: integer + format: int32 + relativePath: + type: + - string + - "null" + path: + type: + - string + - "null" + size: + type: integer + format: int64 + dateAdded: + type: string + format: date-time + sceneName: + type: + - string + - "null" + releaseGroup: + type: + - string + - "null" + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Language" + quality: + $ref: "#/components/schemas/QualityModel" + customFormats: + type: + - array + - "null" + items: + $ref: "#/components/schemas/CustomFormatResource" + customFormatScore: + type: integer + format: int32 + indexerFlags: + type: + - integer + - "null" + format: int32 + releaseType: + $ref: "#/components/schemas/ReleaseType" + mediaInfo: + $ref: "#/components/schemas/MediaInfoResource" + qualityCutoffNotMet: + type: boolean + additionalProperties: false + EpisodeHistoryEventType: + enum: + - unknown + - grabbed + - seriesFolderImported + - downloadFolderImported + - downloadFailed + - episodeFileDeleted + - episodeFileRenamed + - downloadIgnored + type: string + EpisodeResource: + type: object + properties: + id: + type: integer + format: int32 + seriesId: + type: integer + format: int32 + tvdbId: + type: integer + format: int32 + episodeFileId: + type: integer + format: int32 + seasonNumber: + type: integer + format: int32 + episodeNumber: + type: integer + format: int32 + title: + type: + - string + - "null" + airDate: + type: + - string + - "null" + airDateUtc: + type: + - string + - "null" + format: date-time + lastSearchTime: + type: + - string + - "null" + format: date-time + runtime: + type: integer + format: int32 + finaleType: + type: + - string + - "null" + overview: + type: + - string + - "null" + episodeFile: + $ref: "#/components/schemas/EpisodeFileResource" + hasFile: + type: boolean + monitored: + type: boolean + absoluteEpisodeNumber: + type: + - integer + - "null" + format: int32 + sceneAbsoluteEpisodeNumber: + type: + - integer + - "null" + format: int32 + sceneEpisodeNumber: + type: + - integer + - "null" + format: int32 + sceneSeasonNumber: + type: + - integer + - "null" + format: int32 + unverifiedSceneNumbering: + type: boolean + endTime: + type: + - string + - "null" + format: date-time + grabDate: + type: + - string + - "null" + format: date-time + series: + $ref: "#/components/schemas/SeriesResource" + images: + type: + - array + - "null" + items: + $ref: "#/components/schemas/MediaCover" + additionalProperties: false + EpisodeResourcePagingResource: + type: object + properties: + page: + type: integer + format: int32 + pageSize: + type: integer + format: int32 + sortKey: + type: + - string + - "null" + sortDirection: + $ref: "#/components/schemas/SortDirection" + totalRecords: + type: integer + format: int32 + records: + type: + - array + - "null" + items: + $ref: "#/components/schemas/EpisodeResource" + additionalProperties: false + EpisodeTitleRequiredType: + enum: + - always + - bulkSeasonReleases + - never + type: string + EpisodesMonitoredResource: + type: object + properties: + episodeIds: + type: + - array + - "null" + items: + type: integer + format: int32 + monitored: + type: boolean + additionalProperties: false + Field: + type: object + properties: + order: + type: integer + format: int32 + name: + type: + - string + - "null" + label: + type: + - string + - "null" + unit: + type: + - string + - "null" + helpText: + type: + - string + - "null" + helpTextWarning: + type: + - string + - "null" + helpLink: + type: + - string + - "null" + value: + nullable: true + type: + type: + - string + - "null" + advanced: + type: boolean + selectOptions: + type: + - array + - "null" + items: + $ref: "#/components/schemas/SelectOption" + selectOptionsProviderAction: + type: + - string + - "null" + section: + type: + - string + - "null" + hidden: + type: + - string + - "null" + privacy: + $ref: "#/components/schemas/PrivacyLevel" + placeholder: + type: + - string + - "null" + isFloat: + type: boolean + additionalProperties: false + FileDateType: + enum: + - none + - localAirDate + - utcAirDate + type: string + HealthCheckResult: + enum: + - ok + - notice + - warning + - error + type: string + HealthResource: + type: object + properties: + id: + type: integer + format: int32 + source: + type: + - string + - "null" + type: + $ref: "#/components/schemas/HealthCheckResult" + message: + type: + - string + - "null" + wikiUrl: + $ref: "#/components/schemas/HttpUri" + additionalProperties: false + HistoryResource: + type: object + properties: + id: + type: integer + format: int32 + episodeId: + type: integer + format: int32 + seriesId: + type: integer + format: int32 + sourceTitle: + type: + - string + - "null" + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Language" + quality: + $ref: "#/components/schemas/QualityModel" + customFormats: + type: + - array + - "null" + items: + $ref: "#/components/schemas/CustomFormatResource" + customFormatScore: + type: integer + format: int32 + qualityCutoffNotMet: + type: boolean + date: + type: string + format: date-time + downloadId: + type: + - string + - "null" + eventType: + $ref: "#/components/schemas/EpisodeHistoryEventType" + data: + type: + - object + - "null" + additionalProperties: + type: + - string + - "null" + episode: + $ref: "#/components/schemas/EpisodeResource" + series: + $ref: "#/components/schemas/SeriesResource" + additionalProperties: false + HistoryResourcePagingResource: + type: object + properties: + page: + type: integer + format: int32 + pageSize: + type: integer + format: int32 + sortKey: + type: + - string + - "null" + sortDirection: + $ref: "#/components/schemas/SortDirection" + totalRecords: + type: integer + format: int32 + records: + type: + - array + - "null" + items: + $ref: "#/components/schemas/HistoryResource" + additionalProperties: false + HostConfigResource: + type: object + properties: + id: + type: integer + format: int32 + bindAddress: + type: + - string + - "null" + port: + type: integer + format: int32 + sslPort: + type: integer + format: int32 + enableSsl: + type: boolean + launchBrowser: + type: boolean + authenticationMethod: + $ref: "#/components/schemas/AuthenticationType" + authenticationRequired: + $ref: "#/components/schemas/AuthenticationRequiredType" + analyticsEnabled: + type: boolean + username: + type: + - string + - "null" + password: + type: + - string + - "null" + passwordConfirmation: + type: + - string + - "null" + logLevel: + type: + - string + - "null" + logSizeLimit: + type: integer + format: int32 + consoleLogLevel: + type: + - string + - "null" + branch: + type: + - string + - "null" + apiKey: + type: + - string + - "null" + sslCertPath: + type: + - string + - "null" + sslCertPassword: + type: + - string + - "null" + urlBase: + type: + - string + - "null" + instanceName: + type: + - string + - "null" + applicationUrl: + type: + - string + - "null" + updateAutomatically: + type: boolean + updateMechanism: + $ref: "#/components/schemas/UpdateMechanism" + updateScriptPath: + type: + - string + - "null" + proxyEnabled: + type: boolean + proxyType: + $ref: "#/components/schemas/ProxyType" + proxyHostname: + type: + - string + - "null" + proxyPort: + type: integer + format: int32 + proxyUsername: + type: + - string + - "null" + proxyPassword: + type: + - string + - "null" + proxyBypassFilter: + type: + - string + - "null" + proxyBypassLocalAddresses: + type: boolean + certificateValidation: + $ref: "#/components/schemas/CertificateValidationType" + backupFolder: + type: + - string + - "null" + backupInterval: + type: integer + format: int32 + backupRetention: + type: integer + format: int32 + trustCgnatIpAddresses: + type: boolean + additionalProperties: false + HttpUri: + type: object + properties: + fullUri: + type: + - string + - "null" + readOnly: true + scheme: + type: + - string + - "null" + readOnly: true + host: + type: + - string + - "null" + readOnly: true + port: + type: + - integer + - "null" + format: int32 + readOnly: true + path: + type: + - string + - "null" + readOnly: true + query: + type: + - string + - "null" + readOnly: true + fragment: + type: + - string + - "null" + readOnly: true + additionalProperties: false + ImportListBulkResource: + type: object + properties: + ids: + type: + - array + - "null" + items: + type: integer + format: int32 + tags: + type: + - array + - "null" + items: + type: integer + format: int32 + applyTags: + $ref: "#/components/schemas/ApplyTags" + enableAutomaticAdd: + type: + - boolean + - "null" + rootFolderPath: + type: + - string + - "null" + qualityProfileId: + type: + - integer + - "null" + format: int32 + additionalProperties: false + ImportListConfigResource: + type: object + properties: + id: + type: integer + format: int32 + listSyncLevel: + $ref: "#/components/schemas/ListSyncLevelType" + listSyncTag: + type: integer + format: int32 + additionalProperties: false + ImportListExclusionBulkResource: + type: object + properties: + ids: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + additionalProperties: false + ImportListExclusionResource: + type: object + properties: + id: + type: integer + format: int32 + tvdbId: + type: integer + format: int32 + title: + type: + - string + - "null" + additionalProperties: false + ImportListExclusionResourcePagingResource: + type: object + properties: + page: + type: integer + format: int32 + pageSize: + type: integer + format: int32 + sortKey: + type: + - string + - "null" + sortDirection: + $ref: "#/components/schemas/SortDirection" + totalRecords: + type: integer + format: int32 + records: + type: + - array + - "null" + items: + $ref: "#/components/schemas/ImportListExclusionResource" + additionalProperties: false + ImportListResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + fields: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Field" + implementationName: + type: + - string + - "null" + implementation: + type: + - string + - "null" + configContract: + type: + - string + - "null" + infoLink: + type: + - string + - "null" + message: + $ref: "#/components/schemas/ProviderMessage" + tags: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + presets: + type: + - array + - "null" + items: + $ref: "#/components/schemas/ImportListResource" + enableAutomaticAdd: + type: boolean + searchForMissingEpisodes: + type: boolean + shouldMonitor: + $ref: "#/components/schemas/MonitorTypes" + monitorNewItems: + $ref: "#/components/schemas/NewItemMonitorTypes" + rootFolderPath: + type: + - string + - "null" + qualityProfileId: + type: integer + format: int32 + seriesType: + $ref: "#/components/schemas/SeriesTypes" + seasonFolder: + type: boolean + listType: + $ref: "#/components/schemas/ImportListType" + listOrder: + type: integer + format: int32 + minRefreshInterval: + type: string + format: date-span + additionalProperties: false + ImportListType: + enum: + - program + - plex + - trakt + - simkl + - other + - advanced + type: string + ImportRejectionResource: + type: object + properties: + reason: + type: + - string + - "null" + type: + $ref: "#/components/schemas/RejectionType" + additionalProperties: false + IndexerBulkResource: + type: object + properties: + ids: + type: + - array + - "null" + items: + type: integer + format: int32 + tags: + type: + - array + - "null" + items: + type: integer + format: int32 + applyTags: + $ref: "#/components/schemas/ApplyTags" + enableRss: + type: + - boolean + - "null" + enableAutomaticSearch: + type: + - boolean + - "null" + enableInteractiveSearch: + type: + - boolean + - "null" + priority: + type: + - integer + - "null" + format: int32 + additionalProperties: false + IndexerConfigResource: + type: object + properties: + id: + type: integer + format: int32 + minimumAge: + type: integer + format: int32 + retention: + type: integer + format: int32 + maximumSize: + type: integer + format: int32 + rssSyncInterval: + type: integer + format: int32 + additionalProperties: false + IndexerFlagResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + nameLower: + type: + - string + - "null" + readOnly: true + additionalProperties: false + IndexerResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + fields: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Field" + implementationName: + type: + - string + - "null" + implementation: + type: + - string + - "null" + configContract: + type: + - string + - "null" + infoLink: + type: + - string + - "null" + message: + $ref: "#/components/schemas/ProviderMessage" + tags: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + presets: + type: + - array + - "null" + items: + $ref: "#/components/schemas/IndexerResource" + enableRss: + type: boolean + enableAutomaticSearch: + type: boolean + enableInteractiveSearch: + type: boolean + supportsRss: + type: boolean + supportsSearch: + type: boolean + protocol: + $ref: "#/components/schemas/DownloadProtocol" + priority: + type: integer + format: int32 + seasonSearchMaximumSingleEpisodeAge: + type: integer + format: int32 + downloadClientId: + type: integer + format: int32 + additionalProperties: false + Language: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + additionalProperties: false + LanguageProfileItemResource: + type: object + properties: + id: + type: integer + format: int32 + language: + $ref: "#/components/schemas/Language" + allowed: + type: boolean + additionalProperties: false + LanguageProfileResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + upgradeAllowed: + type: boolean + cutoff: + $ref: "#/components/schemas/Language" + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/LanguageProfileItemResource" + additionalProperties: false + LanguageResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + nameLower: + type: + - string + - "null" + readOnly: true + additionalProperties: false + ListSyncLevelType: + enum: + - disabled + - logOnly + - keepAndUnmonitor + - keepAndTag + type: string + LocalizationLanguageResource: + type: object + properties: + identifier: + type: + - string + - "null" + additionalProperties: false + LocalizationResource: + type: object + properties: + id: + type: integer + format: int32 + strings: + type: + - object + - "null" + additionalProperties: + type: + - string + - "null" + additionalProperties: false + LogFileResource: + type: object + properties: + id: + type: integer + format: int32 + filename: + type: + - string + - "null" + lastWriteTime: + type: string + format: date-time + contentsUrl: + type: + - string + - "null" + downloadUrl: + type: + - string + - "null" + additionalProperties: false + LogResource: + type: object + properties: + id: + type: integer + format: int32 + time: + type: string + format: date-time + exception: + type: + - string + - "null" + exceptionType: + type: + - string + - "null" + level: + type: + - string + - "null" + logger: + type: + - string + - "null" + message: + type: + - string + - "null" + method: + type: + - string + - "null" + additionalProperties: false + LogResourcePagingResource: + type: object + properties: + page: + type: integer + format: int32 + pageSize: + type: integer + format: int32 + sortKey: + type: + - string + - "null" + sortDirection: + $ref: "#/components/schemas/SortDirection" + totalRecords: + type: integer + format: int32 + records: + type: + - array + - "null" + items: + $ref: "#/components/schemas/LogResource" + additionalProperties: false + ManualImportReprocessResource: + type: object + properties: + id: + type: integer + format: int32 + path: + type: + - string + - "null" + seriesId: + type: integer + format: int32 + seasonNumber: + type: + - integer + - "null" + format: int32 + episodes: + type: + - array + - "null" + items: + $ref: "#/components/schemas/EpisodeResource" + episodeIds: + type: + - array + - "null" + items: + type: integer + format: int32 + quality: + $ref: "#/components/schemas/QualityModel" + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Language" + releaseGroup: + type: + - string + - "null" + downloadId: + type: + - string + - "null" + customFormats: + type: + - array + - "null" + items: + $ref: "#/components/schemas/CustomFormatResource" + customFormatScore: + type: integer + format: int32 + indexerFlags: + type: integer + format: int32 + releaseType: + $ref: "#/components/schemas/ReleaseType" + rejections: + type: + - array + - "null" + items: + $ref: "#/components/schemas/ImportRejectionResource" + additionalProperties: false + ManualImportResource: + type: object + properties: + id: + type: integer + format: int32 + path: + type: + - string + - "null" + relativePath: + type: + - string + - "null" + folderName: + type: + - string + - "null" + name: + type: + - string + - "null" + size: + type: integer + format: int64 + series: + $ref: "#/components/schemas/SeriesResource" + seasonNumber: + type: + - integer + - "null" + format: int32 + episodes: + type: + - array + - "null" + items: + $ref: "#/components/schemas/EpisodeResource" + episodeFileId: + type: + - integer + - "null" + format: int32 + releaseGroup: + type: + - string + - "null" + quality: + $ref: "#/components/schemas/QualityModel" + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Language" + qualityWeight: + type: integer + format: int32 + downloadId: + type: + - string + - "null" + customFormats: + type: + - array + - "null" + items: + $ref: "#/components/schemas/CustomFormatResource" + customFormatScore: + type: integer + format: int32 + indexerFlags: + type: integer + format: int32 + releaseType: + $ref: "#/components/schemas/ReleaseType" + rejections: + type: + - array + - "null" + items: + $ref: "#/components/schemas/ImportRejectionResource" + additionalProperties: false + MediaCover: + type: object + properties: + coverType: + $ref: "#/components/schemas/MediaCoverTypes" + url: + type: + - string + - "null" + remoteUrl: + type: + - string + - "null" + additionalProperties: false + MediaCoverTypes: + enum: + - unknown + - poster + - banner + - fanart + - screenshot + - headshot + - clearlogo + type: string + MediaInfoResource: + type: object + properties: + id: + type: integer + format: int32 + audioBitrate: + type: integer + format: int64 + audioChannels: + type: number + format: double + audioCodec: + type: + - string + - "null" + audioLanguages: + type: + - string + - "null" + audioStreamCount: + type: integer + format: int32 + videoBitDepth: + type: integer + format: int32 + videoBitrate: + type: integer + format: int64 + videoCodec: + type: + - string + - "null" + videoFps: + type: number + format: double + videoDynamicRange: + type: + - string + - "null" + videoDynamicRangeType: + type: + - string + - "null" + resolution: + type: + - string + - "null" + runTime: + type: + - string + - "null" + scanType: + type: + - string + - "null" + subtitles: + type: + - string + - "null" + additionalProperties: false + MediaManagementConfigResource: + type: object + properties: + id: + type: integer + format: int32 + autoUnmonitorPreviouslyDownloadedEpisodes: + type: boolean + recycleBin: + type: + - string + - "null" + recycleBinCleanupDays: + type: integer + format: int32 + downloadPropersAndRepacks: + $ref: "#/components/schemas/ProperDownloadTypes" + createEmptySeriesFolders: + type: boolean + deleteEmptyFolders: + type: boolean + fileDate: + $ref: "#/components/schemas/FileDateType" + rescanAfterRefresh: + $ref: "#/components/schemas/RescanAfterRefreshType" + setPermissionsLinux: + type: boolean + chmodFolder: + type: + - string + - "null" + chownGroup: + type: + - string + - "null" + episodeTitleRequired: + $ref: "#/components/schemas/EpisodeTitleRequiredType" + skipFreeSpaceCheckWhenImporting: + type: boolean + minimumFreeSpaceWhenImporting: + type: integer + format: int32 + copyUsingHardlinks: + type: boolean + useScriptImport: + type: boolean + scriptImportPath: + type: + - string + - "null" + importExtraFiles: + type: boolean + extraFileExtensions: + type: + - string + - "null" + enableMediaInfo: + type: boolean + additionalProperties: false + MetadataResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + fields: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Field" + implementationName: + type: + - string + - "null" + implementation: + type: + - string + - "null" + configContract: + type: + - string + - "null" + infoLink: + type: + - string + - "null" + message: + $ref: "#/components/schemas/ProviderMessage" + tags: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + presets: + type: + - array + - "null" + items: + $ref: "#/components/schemas/MetadataResource" + enable: + type: boolean + additionalProperties: false + MonitorTypes: + enum: + - unknown + - all + - future + - missing + - existing + - firstSeason + - lastSeason + - latestSeason + - pilot + - recent + - monitorSpecials + - unmonitorSpecials + - none + - skip + type: string + MonitoringOptions: + type: object + properties: + ignoreEpisodesWithFiles: + type: boolean + ignoreEpisodesWithoutFiles: + type: boolean + monitor: + $ref: "#/components/schemas/MonitorTypes" + additionalProperties: false + NamingConfigResource: + type: object + properties: + id: + type: integer + format: int32 + renameEpisodes: + type: boolean + replaceIllegalCharacters: + type: boolean + colonReplacementFormat: + type: integer + format: int32 + customColonReplacementFormat: + type: + - string + - "null" + multiEpisodeStyle: + type: integer + format: int32 + standardEpisodeFormat: + type: + - string + - "null" + dailyEpisodeFormat: + type: + - string + - "null" + animeEpisodeFormat: + type: + - string + - "null" + seriesFolderFormat: + type: + - string + - "null" + seasonFolderFormat: + type: + - string + - "null" + specialsFolderFormat: + type: + - string + - "null" + additionalProperties: false + NewItemMonitorTypes: + enum: + - all + - none + type: string + NotificationResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + fields: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Field" + implementationName: + type: + - string + - "null" + implementation: + type: + - string + - "null" + configContract: + type: + - string + - "null" + infoLink: + type: + - string + - "null" + message: + $ref: "#/components/schemas/ProviderMessage" + tags: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + presets: + type: + - array + - "null" + items: + $ref: "#/components/schemas/NotificationResource" + link: + type: + - string + - "null" + onGrab: + type: boolean + onDownload: + type: boolean + onUpgrade: + type: boolean + onImportComplete: + type: boolean + onRename: + type: boolean + onSeriesAdd: + type: boolean + onSeriesDelete: + type: boolean + onEpisodeFileDelete: + type: boolean + onEpisodeFileDeleteForUpgrade: + type: boolean + onHealthIssue: + type: boolean + includeHealthWarnings: + type: boolean + onHealthRestored: + type: boolean + onApplicationUpdate: + type: boolean + onManualInteractionRequired: + type: boolean + supportsOnGrab: + type: boolean + supportsOnDownload: + type: boolean + supportsOnUpgrade: + type: boolean + supportsOnImportComplete: + type: boolean + supportsOnRename: + type: boolean + supportsOnSeriesAdd: + type: boolean + supportsOnSeriesDelete: + type: boolean + supportsOnEpisodeFileDelete: + type: boolean + supportsOnEpisodeFileDeleteForUpgrade: + type: boolean + supportsOnHealthIssue: + type: boolean + supportsOnHealthRestored: + type: boolean + supportsOnApplicationUpdate: + type: boolean + supportsOnManualInteractionRequired: + type: boolean + testCommand: + type: + - string + - "null" + additionalProperties: false + ParseResource: + type: object + properties: + id: + type: integer + format: int32 + title: + type: + - string + - "null" + parsedEpisodeInfo: + $ref: "#/components/schemas/ParsedEpisodeInfo" + series: + $ref: "#/components/schemas/SeriesResource" + episodes: + type: + - array + - "null" + items: + $ref: "#/components/schemas/EpisodeResource" + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Language" + customFormats: + type: + - array + - "null" + items: + $ref: "#/components/schemas/CustomFormatResource" + customFormatScore: + type: integer + format: int32 + additionalProperties: false + ParsedEpisodeInfo: + type: object + properties: + releaseTitle: + type: + - string + - "null" + seriesTitle: + type: + - string + - "null" + seriesTitleInfo: + $ref: "#/components/schemas/SeriesTitleInfo" + quality: + $ref: "#/components/schemas/QualityModel" + seasonNumber: + type: integer + format: int32 + episodeNumbers: + type: + - array + - "null" + items: + type: integer + format: int32 + absoluteEpisodeNumbers: + type: + - array + - "null" + items: + type: integer + format: int32 + specialAbsoluteEpisodeNumbers: + type: + - array + - "null" + items: + type: number + format: double + airDate: + type: + - string + - "null" + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Language" + fullSeason: + type: boolean + isPartialSeason: + type: boolean + isMultiSeason: + type: boolean + isSeasonExtra: + type: boolean + isSplitEpisode: + type: boolean + isMiniSeries: + type: boolean + special: + type: boolean + releaseGroup: + type: + - string + - "null" + releaseHash: + type: + - string + - "null" + seasonPart: + type: integer + format: int32 + releaseTokens: + type: + - string + - "null" + dailyPart: + type: + - integer + - "null" + format: int32 + isDaily: + type: boolean + readOnly: true + isAbsoluteNumbering: + type: boolean + readOnly: true + isPossibleSpecialEpisode: + type: boolean + readOnly: true + isPossibleSceneSeasonSpecial: + type: boolean + readOnly: true + releaseType: + $ref: "#/components/schemas/ReleaseType" + additionalProperties: false + PingResource: + type: object + properties: + status: + type: + - string + - "null" + additionalProperties: false + PrivacyLevel: + enum: + - normal + - password + - apiKey + - userName + type: string + ProfileFormatItemResource: + type: object + properties: + id: + type: integer + format: int32 + format: + type: integer + format: int32 + name: + type: + - string + - "null" + score: + type: integer + format: int32 + additionalProperties: false + ProperDownloadTypes: + enum: + - preferAndUpgrade + - doNotUpgrade + - doNotPrefer + type: string + ProviderMessage: + type: object + properties: + message: + type: + - string + - "null" + type: + $ref: "#/components/schemas/ProviderMessageType" + additionalProperties: false + ProviderMessageType: + enum: + - info + - warning + - error + type: string + ProxyType: + enum: + - http + - socks4 + - socks5 + type: string + Quality: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + source: + $ref: "#/components/schemas/QualitySource" + resolution: + type: integer + format: int32 + additionalProperties: false + QualityDefinitionLimitsResource: + type: object + properties: + min: + type: integer + format: int32 + max: + type: integer + format: int32 + additionalProperties: false + QualityDefinitionResource: + type: object + properties: + id: + type: integer + format: int32 + quality: + $ref: "#/components/schemas/Quality" + title: + type: + - string + - "null" + weight: + type: integer + format: int32 + minSize: + type: + - number + - "null" + format: double + maxSize: + type: + - number + - "null" + format: double + preferredSize: + type: + - number + - "null" + format: double + additionalProperties: false + QualityModel: + type: object + properties: + quality: + $ref: "#/components/schemas/Quality" + revision: + $ref: "#/components/schemas/Revision" + additionalProperties: false + QualityProfileQualityItemResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + quality: + $ref: "#/components/schemas/Quality" + items: + type: + - array + - "null" + items: + $ref: "#/components/schemas/QualityProfileQualityItemResource" + allowed: + type: boolean + additionalProperties: false + QualityProfileResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + upgradeAllowed: + type: boolean + cutoff: + type: integer + format: int32 + items: + type: + - array + - "null" + items: + $ref: "#/components/schemas/QualityProfileQualityItemResource" + minFormatScore: + type: integer + format: int32 + cutoffFormatScore: + type: integer + format: int32 + minUpgradeFormatScore: + type: integer + format: int32 + formatItems: + type: + - array + - "null" + items: + $ref: "#/components/schemas/ProfileFormatItemResource" + additionalProperties: false + QualitySource: + enum: + - unknown + - television + - televisionRaw + - web + - webRip + - dvd + - bluray + - blurayRaw + type: string + QueueBulkResource: + type: object + properties: + ids: + type: + - array + - "null" + items: + type: integer + format: int32 + additionalProperties: false + QueueResource: + type: object + properties: + id: + type: integer + format: int32 + seriesId: + type: + - integer + - "null" + format: int32 + episodeId: + type: + - integer + - "null" + format: int32 + seasonNumber: + type: + - integer + - "null" + format: int32 + series: + $ref: "#/components/schemas/SeriesResource" + episode: + $ref: "#/components/schemas/EpisodeResource" + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Language" + quality: + $ref: "#/components/schemas/QualityModel" + customFormats: + type: + - array + - "null" + items: + $ref: "#/components/schemas/CustomFormatResource" + customFormatScore: + type: integer + format: int32 + size: + type: number + format: double + title: + type: + - string + - "null" + estimatedCompletionTime: + type: + - string + - "null" + format: date-time + added: + type: + - string + - "null" + format: date-time + status: + $ref: "#/components/schemas/QueueStatus" + trackedDownloadStatus: + $ref: "#/components/schemas/TrackedDownloadStatus" + trackedDownloadState: + $ref: "#/components/schemas/TrackedDownloadState" + statusMessages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/TrackedDownloadStatusMessage" + errorMessage: + type: + - string + - "null" + downloadId: + type: + - string + - "null" + protocol: + $ref: "#/components/schemas/DownloadProtocol" + downloadClient: + type: + - string + - "null" + downloadClientHasPostImportCategory: + type: boolean + indexer: + type: + - string + - "null" + outputPath: + type: + - string + - "null" + episodeHasFile: + type: boolean + sizeleft: + type: number + format: double + deprecated: true + timeleft: + type: + - string + - "null" + format: date-span + deprecated: true + additionalProperties: false + QueueResourcePagingResource: + type: object + properties: + page: + type: integer + format: int32 + pageSize: + type: integer + format: int32 + sortKey: + type: + - string + - "null" + sortDirection: + $ref: "#/components/schemas/SortDirection" + totalRecords: + type: integer + format: int32 + records: + type: + - array + - "null" + items: + $ref: "#/components/schemas/QueueResource" + additionalProperties: false + QueueStatus: + enum: + - unknown + - queued + - paused + - downloading + - completed + - failed + - warning + - delay + - downloadClientUnavailable + - fallback + type: string + QueueStatusResource: + type: object + properties: + id: + type: integer + format: int32 + totalCount: + type: integer + format: int32 + count: + type: integer + format: int32 + unknownCount: + type: integer + format: int32 + errors: + type: boolean + warnings: + type: boolean + unknownErrors: + type: boolean + unknownWarnings: + type: boolean + additionalProperties: false + Ratings: + type: object + properties: + votes: + type: integer + format: int32 + value: + type: number + format: double + additionalProperties: false + RejectionType: + enum: + - permanent + - temporary + type: string + ReleaseEpisodeResource: + type: object + properties: + id: + type: integer + format: int32 + seasonNumber: + type: integer + format: int32 + episodeNumber: + type: integer + format: int32 + absoluteEpisodeNumber: + type: + - integer + - "null" + format: int32 + title: + type: + - string + - "null" + additionalProperties: false + ReleaseProfileResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + enabled: + type: boolean + required: + nullable: true + ignored: + nullable: true + indexerId: + type: integer + format: int32 + tags: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + additionalProperties: false + ReleaseResource: + type: object + properties: + id: + type: integer + format: int32 + guid: + type: + - string + - "null" + quality: + $ref: "#/components/schemas/QualityModel" + qualityWeight: + type: integer + format: int32 + age: + type: integer + format: int32 + ageHours: + type: number + format: double + ageMinutes: + type: number + format: double + size: + type: integer + format: int64 + indexerId: + type: integer + format: int32 + indexer: + type: + - string + - "null" + releaseGroup: + type: + - string + - "null" + subGroup: + type: + - string + - "null" + releaseHash: + type: + - string + - "null" + title: + type: + - string + - "null" + fullSeason: + type: boolean + sceneSource: + type: boolean + seasonNumber: + type: integer + format: int32 + languages: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Language" + languageWeight: + type: integer + format: int32 + airDate: + type: + - string + - "null" + seriesTitle: + type: + - string + - "null" + episodeNumbers: + type: + - array + - "null" + items: + type: integer + format: int32 + absoluteEpisodeNumbers: + type: + - array + - "null" + items: + type: integer + format: int32 + mappedSeasonNumber: + type: + - integer + - "null" + format: int32 + mappedEpisodeNumbers: + type: + - array + - "null" + items: + type: integer + format: int32 + mappedAbsoluteEpisodeNumbers: + type: + - array + - "null" + items: + type: integer + format: int32 + mappedSeriesId: + type: + - integer + - "null" + format: int32 + mappedEpisodeInfo: + type: + - array + - "null" + items: + $ref: "#/components/schemas/ReleaseEpisodeResource" + approved: + type: boolean + temporarilyRejected: + type: boolean + rejected: + type: boolean + tvdbId: + type: integer + format: int32 + tvRageId: + type: integer + format: int32 + imdbId: + type: + - string + - "null" + rejections: + type: + - array + - "null" + items: + type: string + publishDate: + type: string + format: date-time + commentUrl: + type: + - string + - "null" + downloadUrl: + type: + - string + - "null" + infoUrl: + type: + - string + - "null" + episodeRequested: + type: boolean + downloadAllowed: + type: boolean + releaseWeight: + type: integer + format: int32 + customFormats: + type: + - array + - "null" + items: + $ref: "#/components/schemas/CustomFormatResource" + customFormatScore: + type: integer + format: int32 + sceneMapping: + $ref: "#/components/schemas/AlternateTitleResource" + magnetUrl: + type: + - string + - "null" + infoHash: + type: + - string + - "null" + seeders: + type: + - integer + - "null" + format: int32 + leechers: + type: + - integer + - "null" + format: int32 + protocol: + $ref: "#/components/schemas/DownloadProtocol" + indexerFlags: + type: integer + format: int32 + isDaily: + type: boolean + isAbsoluteNumbering: + type: boolean + isPossibleSpecialEpisode: + type: boolean + special: + type: boolean + seriesId: + type: + - integer + - "null" + format: int32 + episodeId: + type: + - integer + - "null" + format: int32 + episodeIds: + type: + - array + - "null" + items: + type: integer + format: int32 + downloadClientId: + type: + - integer + - "null" + format: int32 + downloadClient: + type: + - string + - "null" + shouldOverride: + type: + - boolean + - "null" + additionalProperties: false + ReleaseType: + enum: + - unknown + - singleEpisode + - multiEpisode + - seasonPack + type: string + RemotePathMappingResource: + type: object + properties: + id: + type: integer + format: int32 + host: + type: + - string + - "null" + remotePath: + type: + - string + - "null" + localPath: + type: + - string + - "null" + additionalProperties: false + RenameEpisodeResource: + type: object + properties: + id: + type: integer + format: int32 + seriesId: + type: integer + format: int32 + seasonNumber: + type: integer + format: int32 + episodeNumbers: + type: + - array + - "null" + items: + type: integer + format: int32 + episodeFileId: + type: integer + format: int32 + existingPath: + type: + - string + - "null" + newPath: + type: + - string + - "null" + additionalProperties: false + RescanAfterRefreshType: + enum: + - always + - afterManual + - never + type: string + Revision: + type: object + properties: + version: + type: integer + format: int32 + real: + type: integer + format: int32 + isRepack: + type: boolean + additionalProperties: false + RootFolderResource: + type: object + properties: + id: + type: integer + format: int32 + path: + type: + - string + - "null" + accessible: + type: boolean + freeSpace: + type: + - integer + - "null" + format: int64 + unmappedFolders: + type: + - array + - "null" + items: + $ref: "#/components/schemas/UnmappedFolder" + additionalProperties: false + RuntimeMode: + enum: + - console + - service + - tray + type: string + SeasonPassResource: + type: object + properties: + series: + type: + - array + - "null" + items: + $ref: "#/components/schemas/SeasonPassSeriesResource" + monitoringOptions: + $ref: "#/components/schemas/MonitoringOptions" + additionalProperties: false + SeasonPassSeriesResource: + type: object + properties: + id: + type: integer + format: int32 + monitored: + type: + - boolean + - "null" + seasons: + type: + - array + - "null" + items: + $ref: "#/components/schemas/SeasonResource" + additionalProperties: false + SeasonResource: + type: object + properties: + seasonNumber: + type: integer + format: int32 + monitored: + type: boolean + statistics: + $ref: "#/components/schemas/SeasonStatisticsResource" + images: + type: + - array + - "null" + items: + $ref: "#/components/schemas/MediaCover" + additionalProperties: false + SeasonStatisticsResource: + type: object + properties: + nextAiring: + type: + - string + - "null" + format: date-time + previousAiring: + type: + - string + - "null" + format: date-time + episodeFileCount: + type: integer + format: int32 + episodeCount: + type: integer + format: int32 + totalEpisodeCount: + type: integer + format: int32 + sizeOnDisk: + type: integer + format: int64 + releaseGroups: + type: + - array + - "null" + items: + type: string + percentOfEpisodes: + type: number + format: double + readOnly: true + additionalProperties: false + SelectOption: + type: object + properties: + value: + type: integer + format: int32 + name: + type: + - string + - "null" + order: + type: integer + format: int32 + hint: + type: + - string + - "null" + additionalProperties: false + SeriesEditorResource: + type: object + properties: + seriesIds: + type: + - array + - "null" + items: + type: integer + format: int32 + monitored: + type: + - boolean + - "null" + monitorNewItems: + $ref: "#/components/schemas/NewItemMonitorTypes" + qualityProfileId: + type: + - integer + - "null" + format: int32 + seriesType: + $ref: "#/components/schemas/SeriesTypes" + seasonFolder: + type: + - boolean + - "null" + rootFolderPath: + type: + - string + - "null" + tags: + type: + - array + - "null" + items: + type: integer + format: int32 + applyTags: + $ref: "#/components/schemas/ApplyTags" + moveFiles: + type: boolean + deleteFiles: + type: boolean + addImportListExclusion: + type: boolean + additionalProperties: false + SeriesResource: + type: object + properties: + id: + type: integer + format: int32 + title: + type: + - string + - "null" + alternateTitles: + type: + - array + - "null" + items: + $ref: "#/components/schemas/AlternateTitleResource" + sortTitle: + type: + - string + - "null" + status: + $ref: "#/components/schemas/SeriesStatusType" + ended: + type: boolean + readOnly: true + profileName: + type: + - string + - "null" + overview: + type: + - string + - "null" + nextAiring: + type: + - string + - "null" + format: date-time + previousAiring: + type: + - string + - "null" + format: date-time + network: + type: + - string + - "null" + airTime: + type: + - string + - "null" + images: + type: + - array + - "null" + items: + $ref: "#/components/schemas/MediaCover" + originalLanguage: + $ref: "#/components/schemas/Language" + remotePoster: + type: + - string + - "null" + seasons: + type: + - array + - "null" + items: + $ref: "#/components/schemas/SeasonResource" + year: + type: integer + format: int32 + path: + type: + - string + - "null" + 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 + - "null" + format: date-time + lastAired: + type: + - string + - "null" + format: date-time + seriesType: + $ref: "#/components/schemas/SeriesTypes" + cleanTitle: + type: + - string + - "null" + imdbId: + type: + - string + - "null" + titleSlug: + type: + - string + - "null" + rootFolderPath: + type: + - string + - "null" + folder: + type: + - string + - "null" + certification: + type: + - string + - "null" + genres: + type: + - array + - "null" + items: + type: string + tags: + uniqueItems: true + type: + - array + - "null" + items: + type: integer + format: int32 + added: + type: string + format: date-time + addOptions: + $ref: "#/components/schemas/AddSeriesOptions" + ratings: + $ref: "#/components/schemas/Ratings" + statistics: + $ref: "#/components/schemas/SeriesStatisticsResource" + episodesChanged: + type: + - boolean + - "null" + languageProfileId: + type: integer + format: int32 + readOnly: true + deprecated: 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 + - "null" + items: + type: string + percentOfEpisodes: + type: number + format: double + readOnly: true + additionalProperties: false + SeriesStatusType: + enum: + - continuing + - ended + - upcoming + - deleted + type: string + SeriesTitleInfo: + type: object + properties: + title: + type: + - string + - "null" + titleWithoutYear: + type: + - string + - "null" + year: + type: integer + format: int32 + allTitles: + type: + - array + - "null" + items: + type: string + additionalProperties: false + SeriesTypes: + enum: + - standard + - daily + - anime + type: string + SortDirection: + enum: + - default + - ascending + - descending + type: string + SystemResource: + type: object + properties: + appName: + type: + - string + - "null" + instanceName: + type: + - string + - "null" + version: + type: + - string + - "null" + buildTime: + type: string + format: date-time + isDebug: + type: boolean + isProduction: + type: boolean + isAdmin: + type: boolean + isUserInteractive: + type: boolean + startupPath: + type: + - string + - "null" + appData: + type: + - string + - "null" + osName: + type: + - string + - "null" + osVersion: + type: + - string + - "null" + isNetCore: + type: boolean + isLinux: + type: boolean + isOsx: + type: boolean + isWindows: + type: boolean + isDocker: + type: boolean + mode: + $ref: "#/components/schemas/RuntimeMode" + branch: + type: + - string + - "null" + authentication: + $ref: "#/components/schemas/AuthenticationType" + sqliteVersion: + type: + - string + - "null" + migrationVersion: + type: integer + format: int32 + urlBase: + type: + - string + - "null" + runtimeVersion: + type: + - string + - "null" + runtimeName: + type: + - string + - "null" + startTime: + type: string + format: date-time + packageVersion: + type: + - string + - "null" + packageAuthor: + type: + - string + - "null" + packageUpdateMechanism: + $ref: "#/components/schemas/UpdateMechanism" + packageUpdateMechanismMessage: + type: + - string + - "null" + databaseVersion: + type: + - string + - "null" + databaseType: + $ref: "#/components/schemas/DatabaseType" + additionalProperties: false + TagDetailsResource: + type: object + properties: + id: + type: integer + format: int32 + label: + type: + - string + - "null" + delayProfileIds: + type: + - array + - "null" + items: + type: integer + format: int32 + importListIds: + type: + - array + - "null" + items: + type: integer + format: int32 + notificationIds: + type: + - array + - "null" + items: + type: integer + format: int32 + restrictionIds: + type: + - array + - "null" + items: + type: integer + format: int32 + indexerIds: + type: + - array + - "null" + items: + type: integer + format: int32 + downloadClientIds: + type: + - array + - "null" + items: + type: integer + format: int32 + autoTagIds: + type: + - array + - "null" + items: + type: integer + format: int32 + seriesIds: + type: + - array + - "null" + items: + type: integer + format: int32 + additionalProperties: false + TagResource: + type: object + properties: + id: + type: integer + format: int32 + label: + type: + - string + - "null" + additionalProperties: false + TaskResource: + type: object + properties: + id: + type: integer + format: int32 + name: + type: + - string + - "null" + taskName: + type: + - string + - "null" + interval: + type: integer + format: int32 + lastExecution: + type: string + format: date-time + lastStartTime: + type: string + format: date-time + nextExecution: + type: string + format: date-time + lastDuration: + type: string + format: date-span + readOnly: true + additionalProperties: false + TrackedDownloadState: + enum: + - downloading + - importBlocked + - importPending + - importing + - imported + - failedPending + - failed + - ignored + type: string + TrackedDownloadStatus: + enum: + - ok + - warning + - error + type: string + TrackedDownloadStatusMessage: + type: object + properties: + title: + type: + - string + - "null" + messages: + type: + - array + - "null" + items: + type: string + additionalProperties: false + UiConfigResource: + type: object + properties: + id: + type: integer + format: int32 + firstDayOfWeek: + type: integer + format: int32 + calendarWeekColumnHeader: + type: + - string + - "null" + shortDateFormat: + type: + - string + - "null" + longDateFormat: + type: + - string + - "null" + timeFormat: + type: + - string + - "null" + showRelativeDates: + type: boolean + enableColorImpairedMode: + type: boolean + theme: + type: + - string + - "null" + uiLanguage: + type: integer + format: int32 + additionalProperties: false + UnmappedFolder: + type: object + properties: + name: + type: + - string + - "null" + path: + type: + - string + - "null" + relativePath: + type: + - string + - "null" + additionalProperties: false + UpdateChanges: + type: object + properties: + new: + type: + - array + - "null" + items: + type: string + fixed: + type: + - array + - "null" + items: + type: string + additionalProperties: false + UpdateMechanism: + enum: + - builtIn + - script + - external + - apt + - docker + type: string + UpdateResource: + type: object + properties: + id: + type: integer + format: int32 + version: + type: + - string + - "null" + branch: + type: + - string + - "null" + releaseDate: + type: string + format: date-time + fileName: + type: + - string + - "null" + url: + type: + - string + - "null" + installed: + type: boolean + installedOn: + type: + - string + - "null" + format: date-time + installable: + type: boolean + latest: + type: boolean + changes: + $ref: "#/components/schemas/UpdateChanges" + hash: + type: + - string + - "null" + additionalProperties: false + securitySchemes: + X-Api-Key: + type: apiKey + description: Apikey passed as header + name: X-Api-Key + in: header + apikey: + type: apiKey + description: Apikey passed as query parameter + name: apikey + in: query +security: + - X-Api-Key: [] + - apikey: [] diff --git a/src/tui.rs b/src/tui.rs index bf87361..386cc0d 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -22,7 +22,8 @@ use tokio::sync::mpsc; use crate::config::{AppConfig, KeybindMode}; use yarr_api::{ - Episode, HealthResource, HistoryItem, QueueItem, Series, SonarrClient, SystemStatus, + Episode, HealthResource, HistoryItem, QueueItem, ReleaseResource, Series, SonarrClient, + SystemStatus, }; #[derive(Debug)] @@ -39,6 +40,8 @@ pub enum AppEvent { HealthLoaded(Vec), CalendarLoaded(Vec), SearchResults(Vec), + ReleasesLoaded(Vec), + ReleaseDownloaded(String), } #[derive(Debug, Clone, Copy, PartialEq)] @@ -95,6 +98,7 @@ pub struct App { pub health_list_state: ListState, pub search_list_state: ListState, pub settings_list_state: ListState, + pub releases_list_state: ListState, pub series: Vec, pub episodes: Vec, pub queue: Vec, @@ -117,6 +121,10 @@ pub struct App { pub editing_api_key: bool, pub url_input: String, pub api_key_input: String, + pub show_releases_popup: bool, + pub releases: Vec, + pub selected_search_series: Option, + pub loading_releases: bool, } impl Default for App { @@ -131,6 +139,7 @@ impl Default for App { health_list_state: ListState::default(), search_list_state: ListState::default(), settings_list_state: ListState::default(), + releases_list_state: ListState::default(), series: Vec::new(), episodes: Vec::new(), queue: Vec::new(), @@ -152,9 +161,14 @@ impl Default for App { editing_api_key: false, url_input: String::new(), api_key_input: String::new(), + show_releases_popup: false, + releases: Vec::new(), + selected_search_series: None, + loading_releases: false, }; app.series_list_state.select(Some(0)); app.settings_list_state.select(Some(0)); + app.releases_list_state.select(Some(0)); app } } @@ -177,6 +191,11 @@ impl App { } pub fn next_item(&mut self) { + if self.show_releases_popup { + self.next_release(); + return; + } + match self.current_tab { TabIndex::Series => { let len = self.series.len(); @@ -245,6 +264,11 @@ impl App { } pub fn previous_item(&mut self) { + if self.show_releases_popup { + self.previous_release(); + return; + } + match self.current_tab { TabIndex::Series => { let len = self.series.len(); @@ -366,6 +390,55 @@ impl App { } } + pub fn get_selected_release(&self) -> Option<&ReleaseResource> { + if let Some(index) = self.releases_list_state.selected() { + self.releases.get(index) + } else { + None + } + } + + pub fn show_releases_popup(&mut self, series: Series) { + self.show_releases_popup = true; + self.selected_search_series = Some(series); + self.releases.clear(); + self.releases_list_state.select(Some(0)); + self.loading_releases = true; + } + + pub fn hide_releases_popup(&mut self) { + self.show_releases_popup = false; + self.selected_search_series = None; + self.releases.clear(); + self.loading_releases = false; + } + + pub fn next_release(&mut self) { + if !self.releases.is_empty() { + let i = match self.releases_list_state.selected() { + Some(i) => (i + 1) % self.releases.len(), + None => 0, + }; + self.releases_list_state.select(Some(i)); + } + } + + pub fn previous_release(&mut self) { + if !self.releases.is_empty() { + let i = match self.releases_list_state.selected() { + Some(i) => { + if i == 0 { + self.releases.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.releases_list_state.select(Some(i)); + } + } + pub fn enter_search_mode(&mut self) { self.search_mode = true; self.input_mode = true; @@ -721,6 +794,18 @@ async fn run_tui( app.search_list_state.select(Some(0)); } } + AppEvent::ReleasesLoaded(releases) => { + app.releases = releases; + app.loading_releases = false; + if !app.releases.is_empty() { + app.releases_list_state.select(Some(0)); + } + } + AppEvent::ReleaseDownloaded(title) => { + app.hide_releases_popup(); + app.set_error(format!("Successfully started download: {}", title)); + app.loading = false; + } } } } @@ -939,14 +1024,61 @@ async fn handle_normal_mode( } } KeyCode::Enter => { - if app.current_tab == TabIndex::Settings { + if app.show_releases_popup { + // Download the selected release + if let Some(release) = app.get_selected_release().cloned() { + let client_clone = client.clone(); + let tx_clone = tx.clone(); + tokio::spawn(async move { + match client_clone.download_release(&release).await { + Ok(()) => { + let title = release.title.unwrap_or_default(); + let _ = tx_clone.send(AppEvent::ReleaseDownloaded(title)); + } + Err(e) => { + let _ = tx_clone + .send(AppEvent::Error(format!("Download failed: {}", e))); + } + } + }); + app.hide_releases_popup(); + } + } else if app.current_tab == TabIndex::Search && !app.search_results.is_empty() { + // Show releases popup for selected search result + if let Some(selected_series) = app.get_selected_search_result().cloned() { + app.show_releases_popup(selected_series.clone()); + let client_clone = client.clone(); + let tx_clone = tx.clone(); + let series_id = selected_series.id.unwrap_or(0); + tokio::spawn(async move { + match client_clone + .search_releases(Some(series_id), None, None) + .await + { + Ok(releases) => { + let _ = tx_clone.send(AppEvent::ReleasesLoaded(releases)); + } + Err(e) => { + let _ = tx_clone.send(AppEvent::Error(format!( + "Failed to load releases: {}", + e + ))); + } + } + }); + } + } else if app.current_tab == TabIndex::Settings { app.handle_settings_input(); } } KeyCode::Esc => { - app.clear_error(); - if app.search_mode { - app.exit_search_mode(); + if app.show_releases_popup { + app.hide_releases_popup(); + } else { + app.clear_error(); + if app.search_mode { + app.exit_search_mode(); + } } } _ => {} @@ -1063,6 +1195,11 @@ fn ui(f: &mut Frame, app: &App) { // Render footer render_footer(f, chunks[2], app); + // Render releases popup if it should be shown + if app.show_releases_popup { + render_releases_popup(f, app); + } + // Render error popup if there's an error if app.error_message.is_some() { render_error_popup(f, size, app); @@ -1397,7 +1534,7 @@ fn render_search_tab(f: &mut Frame, area: Rect, app: &App) { .block( Block::default() .borders(Borders::ALL) - .title("Search Series (Press '/' to search, Enter to execute)"), + .title("Search Series (Press '/' to search, Enter to execute, Enter on result to view releases)"), ); f.render_widget(input, chunks[0]); @@ -1406,9 +1543,9 @@ fn render_search_tab(f: &mut Frame, area: Rect, app: &App) { let text = if app.loading { "Searching..." } else if app.search_input.is_empty() { - "Enter a search term and press Enter" + "Enter a search term and press Enter to search for series" } else { - "No results found" + "No results found - try a different search term" }; let paragraph = Paragraph::new(text) @@ -1445,7 +1582,7 @@ fn render_search_tab(f: &mut Frame, area: Rect, app: &App) { .block( Block::default() .borders(Borders::ALL) - .title("Search Results"), + .title("Search Results (Press Enter on a series to view available releases)"), ) .highlight_style( Style::default() @@ -1574,7 +1711,9 @@ fn render_settings_input(f: &mut Frame, area: Rect, app: &App) { } fn render_footer(f: &mut Frame, area: Rect, app: &App) { - let help_text = if app.input_mode { + let help_text = if app.show_releases_popup { + "Enter: Download | Esc: Close | ↑↓/jk: Navigate" + } else if app.input_mode { if app.editing_url || app.editing_api_key { "ESC: Cancel | Enter: Save | Type to enter value" } else { @@ -1582,6 +1721,8 @@ fn render_footer(f: &mut Frame, area: Rect, app: &App) { } } else if !app.config.ui.show_help { "" // Don't show help if disabled + } else if app.current_tab == TabIndex::Search && !app.search_results.is_empty() { + "Enter: Show Releases | ↑↓/jk: Navigate | /: Search | Other: Normal keys" } else { match app.config.ui.keybind_mode { KeybindMode::Normal => { @@ -1638,6 +1779,106 @@ fn render_error_popup(f: &mut Frame, area: Rect, app: &App) { } } +fn render_releases_popup(f: &mut Frame, app: &App) { + let area = centered_rect(80, 70, f.area()); + + f.render_widget(Clear, area); + + let title = if let Some(ref series) = app.selected_search_series { + format!( + "Releases for: {}", + series.title.as_deref().unwrap_or("Unknown") + ) + } else { + "Releases".to_string() + }; + + let block = Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Yellow)); + + if app.loading_releases { + let loading_text = Paragraph::new("Loading releases...") + .block(block) + .alignment(Alignment::Center); + f.render_widget(loading_text, area); + } else if app.releases.is_empty() { + let no_releases_text = Paragraph::new("No releases found") + .block(block) + .alignment(Alignment::Center); + f.render_widget(no_releases_text, area); + } else { + let inner_area = block.inner(area); + f.render_widget(block, area); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(3)]) + .split(inner_area); + + let items: Vec = app + .releases + .iter() + .map(|release| { + let title = release.title.as_deref().unwrap_or("Unknown"); + let size = format_size(release.size.unwrap_or(0)); + let quality = release.quality_weight.unwrap_or(0); + let indexer = release.indexer.as_deref().unwrap_or("Unknown"); + let seeds = release.seeders.unwrap_or(0); + let peers = release.leechers.unwrap_or(0); + + let status = if release.download_allowed.unwrap_or(false) { + if release.rejected.unwrap_or(false) { + "❌ Rejected" + } else { + "✅ Available" + } + } else { + "⛔ Not Allowed" + }; + + ListItem::new(format!( + "{} | {} | Q:{} | {} | S:{} P:{} | {}", + status, size, quality, indexer, seeds, peers, title + )) + }) + .collect(); + + let list = List::new(items).highlight_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ); + + f.render_stateful_widget(list, chunks[0], &mut app.releases_list_state.clone()); + + let help_text = Paragraph::new("Enter: Download | Esc: Close | ↑↓: Navigate") + .style(Style::default().fg(Color::Gray)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::TOP)); + f.render_widget(help_text, chunks[1]); + } +} + +fn format_size(bytes: i64) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; + + if bytes == 0 { + return "0 B".to_string(); + } + + let mut size = bytes as f64; + let mut unit_index = 0; + + while size >= 1024.0 && unit_index < UNITS.len() - 1 { + size /= 1024.0; + unit_index += 1; + } + + format!("{:.1} {}", size, UNITS[unit_index]) +} + fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) diff --git a/yarr-api/README.md b/yarr-api/README.md index 9e37f2b..c160d5b 100644 --- a/yarr-api/README.md +++ b/yarr-api/README.md @@ -12,6 +12,8 @@ A Rust client library for the Sonarr API. - **Type-safe API** - All API responses are strongly typed with `serde` - **Error handling** - Comprehensive error types with detailed error information - **Easy to use** - Simple client interface with intuitive method names +- **Download after search** - Search for releases and automatically download the best quality +- **Release management** - Full support for searching, filtering, and downloading releases - **Well documented** - Extensive documentation and examples ## Installation @@ -48,6 +50,12 @@ async fn main() -> Result<()> { let queue = client.get_queue().await?; println!("Items in queue: {}", queue.records.len()); + // Search and download the best release for a series + let downloaded = client.search_and_download_best(Some(1), None, None).await?; + if let Some(release) = downloaded { + println!("Downloaded: {}", release.title.unwrap_or_default()); + } + Ok(()) } ``` @@ -73,6 +81,12 @@ async fn main() -> Result<()> { - ✅ Get download queue - ✅ Get download history +### Releases +- ✅ Search for releases by series/episode/season +- ✅ Download specific releases +- ✅ Automatic best quality selection +- ✅ Advanced filtering and sorting + ## Examples See the `examples/` directory for more comprehensive usage examples: @@ -81,11 +95,39 @@ See the `examples/` directory for more comprehensive usage examples: # Run the basic usage example cargo run --example basic_usage +# Run the download after search example +cargo run --example download_after_search + # Make sure to set your Sonarr URL and API key first: export SONARR_URL="http://localhost:8989" export SONARR_API_KEY="your-api-key-here" ``` +### Download After Search + +The library provides powerful functionality for searching and automatically downloading releases: + +```rust +use yarr_api::SonarrClient; + +let client = SonarrClient::new(url, api_key); + +// Search and download the first available release +let success = client.search_and_download(Some(series_id), None, None).await?; + +// Search and download the best quality release +let best_release = client.search_and_download_best(Some(series_id), None, Some(season)).await?; + +// Manual search with custom filtering +let releases = client.search_releases(Some(series_id), Some(episode_id), None).await?; +for release in releases { + if release.download_allowed.unwrap_or(false) && !release.rejected.unwrap_or(true) { + client.download_release(&release).await?; + break; + } +} +``` + ## Error Handling The library uses a custom `ApiError` type that provides detailed error information: @@ -121,6 +163,9 @@ All Sonarr API responses are represented as strongly-typed Rust structs: - `QueueItem` - Download queue items - `HistoryItem` - Download history - `HealthResource` - Health check results +- `ReleaseResource` - Release/torrent information with download metadata +- `QualityModel` - Quality information and settings +- `CustomFormatResource` - Custom format definitions ## Contributing diff --git a/yarr-api/examples/download_after_search.rs b/yarr-api/examples/download_after_search.rs new file mode 100644 index 0000000..f6aa04d --- /dev/null +++ b/yarr-api/examples/download_after_search.rs @@ -0,0 +1,189 @@ +//! Example demonstrating download after search functionality +//! +//! This example shows how to: +//! 1. Search for available releases for a specific series/episode +//! 2. Download the best quality release automatically +//! 3. Handle different search scenarios + +use std::env; +use yarr_api::{ApiError, SonarrClient}; + +#[tokio::main] +async fn main() -> Result<(), ApiError> { + // Initialize the client with environment variables + let base_url = env::var("SONARR_URL").unwrap_or_else(|_| "http://localhost:8989".to_string()); + let api_key = + env::var("SONARR_API_KEY").expect("SONARR_API_KEY environment variable is required"); + + let client = SonarrClient::new(base_url, api_key); + + println!("🔍 Sonarr Download After Search Example"); + println!("========================================"); + + // Example 1: Search and download best release for a specific series + println!("\n📺 Example 1: Search and download for series ID 1"); + match search_and_download_for_series(&client, 1).await { + Ok(success) => { + if success { + println!("✅ Successfully found and downloaded a release!"); + } else { + println!("❌ No suitable releases found for download"); + } + } + Err(e) => println!("❌ Error: {}", e), + } + + // Example 2: Search releases for a specific episode + println!("\n🎬 Example 2: Search releases for episode ID 123"); + match search_releases_for_episode(&client, 123).await { + Ok(releases) => { + println!("📋 Found {} releases:", releases.len()); + for (i, release) in releases.iter().enumerate().take(5) { + println!( + " {}. {} ({})", + i + 1, + release.title.as_deref().unwrap_or("Unknown"), + format_size(release.size.unwrap_or(0)) + ); + } + } + Err(e) => println!("❌ Error: {}", e), + } + + // Example 3: Search and download best quality for a season + println!("\n📀 Example 3: Search and download best for season 1 of series 1"); + match search_and_download_best_for_season(&client, 1, 1).await { + Ok(release_opt) => match release_opt { + Some(release) => { + println!( + "✅ Downloaded: {}", + release.title.as_deref().unwrap_or("Unknown") + ); + println!(" Quality: {}", release.quality_weight.unwrap_or(0)); + println!(" Size: {}", format_size(release.size.unwrap_or(0))); + } + None => println!("❌ No suitable releases found"), + }, + Err(e) => println!("❌ Error: {}", e), + } + + // Example 4: Manual search and selective download + println!("\n🎯 Example 4: Manual search and filter"); + match manual_search_and_filter(&client, 1).await { + Ok(()) => println!("✅ Manual search completed"), + Err(e) => println!("❌ Error: {}", e), + } + + Ok(()) +} + +/// Search and download the first available release for a series +async fn search_and_download_for_series( + client: &SonarrClient, + series_id: u32, +) -> Result { + client + .search_and_download(Some(series_id), None, None) + .await +} + +/// Search for releases for a specific episode (without downloading) +async fn search_releases_for_episode( + client: &SonarrClient, + episode_id: u32, +) -> Result, ApiError> { + client.search_releases(None, Some(episode_id), None).await +} + +/// Search and download the best quality release for a season +async fn search_and_download_best_for_season( + client: &SonarrClient, + series_id: u32, + season_number: i32, +) -> Result, ApiError> { + client + .search_and_download_best(Some(series_id), None, Some(season_number)) + .await +} + +/// Manual search with custom filtering logic +async fn manual_search_and_filter(client: &SonarrClient, series_id: u32) -> Result<(), ApiError> { + let releases = client.search_releases(Some(series_id), None, None).await?; + + println!("🔍 Found {} total releases", releases.len()); + + // Custom filtering: prefer releases with specific criteria + let filtered_releases: Vec<_> = releases + .into_iter() + .filter(|r| { + // Only downloadable releases + if !r.download_allowed.unwrap_or(false) || r.rejected.unwrap_or(true) { + return false; + } + + // Prefer releases with higher seeds (for torrents) + if let Some(seeders) = r.seeders { + if seeders < 5 { + return false; + } + } + + // Avoid very large files (over 10GB) + if let Some(size) = r.size { + if size > 10 * 1024 * 1024 * 1024 { + return false; + } + } + + true + }) + .collect(); + + println!("📋 {} releases match our criteria", filtered_releases.len()); + + if let Some(best_release) = filtered_releases.first() { + println!( + "🎯 Downloading: {}", + best_release.title.as_deref().unwrap_or("Unknown") + ); + client.download_release(best_release).await?; + println!("✅ Download initiated successfully!"); + } else { + println!("❌ No releases match our filtering criteria"); + } + + Ok(()) +} + +/// Format file size in human-readable format +fn format_size(bytes: i64) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; + + if bytes == 0 { + return "0 B".to_string(); + } + + let mut size = bytes as f64; + let mut unit_index = 0; + + while size >= 1024.0 && unit_index < UNITS.len() - 1 { + size /= 1024.0; + unit_index += 1; + } + + format!("{:.1} {}", size, UNITS[unit_index]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_size() { + assert_eq!(format_size(0), "0 B"); + assert_eq!(format_size(1024), "1.0 KB"); + assert_eq!(format_size(1536), "1.5 KB"); + assert_eq!(format_size(1048576), "1.0 MB"); + assert_eq!(format_size(1073741824), "1.0 GB"); + } +} diff --git a/yarr-api/src/lib.rs b/yarr-api/src/lib.rs index 3e94955..3345518 100644 --- a/yarr-api/src/lib.rs +++ b/yarr-api/src/lib.rs @@ -197,6 +197,264 @@ impl SonarrClient { pub async fn add_series(&self, series: &Series) -> Result { self.post("/series", series).await } + + /// Search for available releases/torrents for download + /// + /// This method searches for releases that match the specified criteria. + /// You can search by series ID, episode ID, season number, or any combination. + /// + /// # Arguments + /// + /// * `series_id` - Optional series ID to search for releases + /// * `episode_id` - Optional specific episode ID to search for releases + /// * `season_number` - Optional season number to search for releases + /// + /// # Returns + /// + /// A vector of `ReleaseResource` objects containing release information including: + /// - Title, size, quality, and indexer information + /// - Download URLs and availability status + /// - Quality scores and custom format information + /// - Rejection reasons (if any) + /// + /// # Examples + /// + /// ```rust,no_run + /// # use yarr_api::{SonarrClient, ApiError}; + /// # #[tokio::main] + /// # async fn main() -> Result<(), ApiError> { + /// let client = SonarrClient::new("http://localhost:8989".to_string(), "api-key".to_string()); + /// + /// // Search for all releases for series ID 1 + /// let releases = client.search_releases(Some(1), None, None).await?; + /// + /// // Search for releases for a specific episode + /// let episode_releases = client.search_releases(None, Some(123), None).await?; + /// + /// // Search for releases for season 2 of series 1 + /// let season_releases = client.search_releases(Some(1), None, Some(2)).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn search_releases( + &self, + series_id: Option, + episode_id: Option, + season_number: Option, + ) -> Result, ApiError> { + let mut query_params = Vec::new(); + + if let Some(id) = series_id { + query_params.push(format!("seriesId={}", id)); + } + if let Some(id) = episode_id { + query_params.push(format!("episodeId={}", id)); + } + if let Some(season) = season_number { + query_params.push(format!("seasonNumber={}", season)); + } + + let endpoint = if query_params.is_empty() { + "/release".to_string() + } else { + format!("/release?{}", query_params.join("&")) + }; + + self.get(&endpoint).await + } + + /// Download/grab a specific release + /// + /// This method instructs Sonarr to download the specified release. + /// The release will be added to the download queue and handled by + /// the configured download client. + /// + /// # Arguments + /// + /// * `release` - The release to download, typically obtained from `search_releases()` + /// + /// # Returns + /// + /// Returns `Ok(())` if the download was successfully queued, or an `ApiError` if: + /// - The release is not downloadable (`download_allowed` is false) + /// - The release has been rejected + /// - There's a network or API error + /// + /// # Examples + /// + /// ```rust,no_run + /// # use yarr_api::{SonarrClient, ApiError}; + /// # #[tokio::main] + /// # async fn main() -> Result<(), ApiError> { + /// let client = SonarrClient::new("http://localhost:8989".to_string(), "api-key".to_string()); + /// + /// let releases = client.search_releases(Some(1), None, None).await?; + /// if let Some(release) = releases.first() { + /// if release.download_allowed.unwrap_or(false) { + /// client.download_release(release).await?; + /// println!("Download started!"); + /// } + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn download_release(&self, release: &ReleaseResource) -> Result<(), ApiError> { + let url = format!("{}/api/v3/release", self.base_url); + let response = self + .client + .post(&url) + .header("X-Api-Key", &self.api_key) + .json(release) + .send() + .await?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); + return Err(ApiError::Generic { + message: format!("HTTP {}: {}", status, error_text), + }); + } + + Ok(()) + } + + /// Search for releases and download the first available one + /// + /// This is a convenience method that combines searching and downloading. + /// It will search for releases matching the criteria and download the first + /// release that is allowed and not rejected. + /// + /// # Arguments + /// + /// * `series_id` - Optional series ID to search for releases + /// * `episode_id` - Optional specific episode ID to search for releases + /// * `season_number` - Optional season number to search for releases + /// + /// # Returns + /// + /// Returns `true` if a suitable release was found and download was initiated, + /// `false` if no suitable releases were available. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use yarr_api::{SonarrClient, ApiError}; + /// # #[tokio::main] + /// # async fn main() -> Result<(), ApiError> { + /// let client = SonarrClient::new("http://localhost:8989".to_string(), "api-key".to_string()); + /// + /// let success = client.search_and_download(Some(1), None, None).await?; + /// if success { + /// println!("Download started!"); + /// } else { + /// println!("No suitable releases found"); + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn search_and_download( + &self, + series_id: Option, + episode_id: Option, + season_number: Option, + ) -> Result { + let releases = self + .search_releases(series_id, episode_id, season_number) + .await?; + + // Find the first downloadable release + for release in releases { + if release.download_allowed.unwrap_or(false) && !release.rejected.unwrap_or(true) { + self.download_release(&release).await?; + return Ok(true); + } + } + + Ok(false) // No suitable release found + } + + /// Search for releases and download the best quality one + /// + /// This method searches for releases and automatically selects and downloads + /// the best quality release based on quality weight and custom format scores. + /// Only releases that are downloadable and not rejected are considered. + /// + /// The selection algorithm prioritizes: + /// 1. Higher quality weight (better video/audio quality) + /// 2. Higher custom format scores (preferred formats/sources) + /// + /// # Arguments + /// + /// * `series_id` - Optional series ID to search for releases + /// * `episode_id` - Optional specific episode ID to search for releases + /// * `season_number` - Optional season number to search for releases + /// + /// # Returns + /// + /// Returns `Some(ReleaseResource)` with the downloaded release information, + /// or `None` if no suitable releases were found. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use yarr_api::{SonarrClient, ApiError}; + /// # #[tokio::main] + /// # async fn main() -> Result<(), ApiError> { + /// let client = SonarrClient::new("http://localhost:8989".to_string(), "api-key".to_string()); + /// + /// match client.search_and_download_best(Some(1), None, Some(2)).await? { + /// Some(release) => { + /// println!("Downloaded: {}", release.title.unwrap_or_default()); + /// println!("Quality: {}", release.quality_weight.unwrap_or(0)); + /// } + /// None => println!("No suitable releases found"), + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn search_and_download_best( + &self, + series_id: Option, + episode_id: Option, + season_number: Option, + ) -> Result, ApiError> { + let releases = self + .search_releases(series_id, episode_id, season_number) + .await?; + + // Filter downloadable releases and sort by quality weight and custom format score + let mut downloadable_releases: Vec<_> = releases + .into_iter() + .filter(|r| r.download_allowed.unwrap_or(false) && !r.rejected.unwrap_or(true)) + .collect(); + + if downloadable_releases.is_empty() { + return Ok(None); + } + + // Sort by quality weight (higher is better) and custom format score (higher is better) + downloadable_releases.sort_by(|a, b| { + let quality_cmp = b + .quality_weight + .unwrap_or(0) + .cmp(&a.quality_weight.unwrap_or(0)); + if quality_cmp == std::cmp::Ordering::Equal { + b.custom_format_score + .unwrap_or(0) + .cmp(&a.custom_format_score.unwrap_or(0)) + } else { + quality_cmp + } + }); + + let best_release = downloadable_releases.into_iter().next().unwrap(); + self.download_release(&best_release).await?; + Ok(Some(best_release)) + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -568,3 +826,89 @@ pub struct HealthResource { pub message: Option, pub wiki_url: Option, } + +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReleaseResource { + pub id: Option, + pub guid: Option, + pub quality: Option, + pub quality_weight: Option, + pub age: Option, + pub age_hours: Option, + pub age_minutes: Option, + pub size: Option, + pub indexer_id: Option, + pub indexer: Option, + pub release_group: Option, + pub sub_group: Option, + pub release_hash: Option, + pub title: Option, + pub full_season: Option, + pub scene_source: Option, + pub season_number: Option, + pub languages: Option>, + pub language_weight: Option, + pub air_date: Option, + pub series_title: Option, + pub episode_numbers: Option>, + pub absolute_episode_numbers: Option>, + pub mapped_season_number: Option, + pub mapped_episode_numbers: Option>, + pub mapped_absolute_episode_numbers: Option>, + pub mapped_series_id: Option, + pub approved: Option, + pub temporarily_rejected: Option, + pub rejected: Option, + pub tvdb_id: Option, + pub tv_rage_id: Option, + pub imdb_id: Option, + pub rejections: Option>, + pub publish_date: Option>, + pub comment_url: Option, + pub download_url: Option, + pub info_url: Option, + pub episode_requested: Option, + pub download_allowed: Option, + pub release_weight: Option, + pub custom_formats: Option>, + pub custom_format_score: Option, + pub magnet_url: Option, + pub info_hash: Option, + pub seeders: Option, + pub leechers: Option, + pub protocol: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct QualityModel { + pub quality: Option, + pub revision: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomFormatResource { + pub id: Option, + pub name: Option, + pub include_custom_format_when_renaming: Option, + pub specifications: Option>>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReleaseEpisodeResource { + pub id: Option, + pub episode_file_id: Option, + pub season_number: Option, + pub episode_number: Option, + pub absolute_episode_number: Option, + pub title: Option, + pub scene_season_number: Option, + pub scene_episode_number: Option, + pub scene_absolute_episode_number: Option, +} diff --git a/yarr-api/src/tests.rs b/yarr-api/src/tests.rs new file mode 100644 index 0000000..af7eb65 --- /dev/null +++ b/yarr-api/src/tests.rs @@ -0,0 +1,336 @@ +//! Unit tests for the yarr-api crate +//! +//! These tests verify the functionality of the Sonarr API client, +//! particularly the download after search features. + +#[cfg(test)] +mod tests { + use crate::{ + CustomFormatResource, Language, QualityDefinition, QualityModel, ReleaseResource, Revision, + SonarrClient, + }; + + fn create_test_client() -> SonarrClient { + SonarrClient::new( + "http://localhost:8989".to_string(), + "test-api-key".to_string(), + ) + } + + fn create_test_release( + id: i32, + title: &str, + quality_weight: i32, + custom_format_score: i32, + download_allowed: bool, + rejected: bool, + size: i64, + ) -> ReleaseResource { + ReleaseResource { + id: Some(id), + guid: Some(format!("test-guid-{}", id)), + quality: Some(QualityModel { + quality: Some(QualityDefinition { + id: 1, + name: Some("HDTV-1080p".to_string()), + source: "HDTV".to_string(), + resolution: 1080, + }), + revision: Some(Revision { + version: 1, + real: 0, + is_repack: false, + }), + }), + quality_weight: Some(quality_weight), + age: Some(1), + age_hours: Some(24.0), + age_minutes: Some(1440.0), + size: Some(size), + indexer_id: Some(1), + indexer: Some("Test Indexer".to_string()), + release_group: Some("TestGroup".to_string()), + sub_group: None, + release_hash: Some("test-hash".to_string()), + title: Some(title.to_string()), + full_season: Some(false), + scene_source: Some(false), + season_number: Some(1), + languages: Some(vec![Language { + id: 1, + name: Some("English".to_string()), + }]), + language_weight: Some(100), + air_date: Some("2024-01-01".to_string()), + series_title: Some("Test Series".to_string()), + episode_numbers: Some(vec![1]), + absolute_episode_numbers: None, + mapped_season_number: Some(1), + mapped_episode_numbers: Some(vec![1]), + mapped_absolute_episode_numbers: None, + mapped_series_id: Some(1), + approved: Some(true), + temporarily_rejected: Some(false), + rejected: Some(rejected), + tvdb_id: Some(12345), + tv_rage_id: Some(67890), + imdb_id: Some("tt1234567".to_string()), + rejections: if rejected { + Some(vec!["Test rejection reason".to_string()]) + } else { + None + }, + publish_date: Some(chrono::Utc::now()), + comment_url: Some("http://example.com/comments".to_string()), + download_url: Some("http://example.com/download".to_string()), + info_url: Some("http://example.com/info".to_string()), + episode_requested: Some(true), + download_allowed: Some(download_allowed), + release_weight: Some(quality_weight + custom_format_score), + custom_formats: Some(vec![CustomFormatResource { + id: Some(1), + name: Some("Test Format".to_string()), + include_custom_format_when_renaming: Some(true), + specifications: None, + }]), + custom_format_score: Some(custom_format_score), + magnet_url: Some("magnet:?xt=urn:btih:test".to_string()), + info_hash: Some("test-info-hash".to_string()), + seeders: Some(10), + leechers: Some(2), + protocol: Some("torrent".to_string()), + } + } + + #[test] + fn test_client_creation() { + let client = create_test_client(); + assert_eq!(client.base_url, "http://localhost:8989"); + assert_eq!(client.api_key, "test-api-key"); + } + + #[test] + fn test_release_resource_serialization() { + let release = create_test_release( + 1, + "Test Release", + 1000, + 100, + true, + false, + 2_000_000_000, // 2GB + ); + + // Test serialization + let json = serde_json::to_string(&release).unwrap(); + assert!(json.contains("Test Release")); + assert!(json.contains("downloadAllowed")); + + // Test deserialization + let deserialized: ReleaseResource = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.title, Some("Test Release".to_string())); + assert_eq!(deserialized.download_allowed, Some(true)); + assert_eq!(deserialized.quality_weight, Some(1000)); + } + + #[test] + fn test_quality_model_creation() { + let quality = QualityModel { + quality: Some(QualityDefinition { + id: 1, + name: Some("HDTV-1080p".to_string()), + source: "HDTV".to_string(), + resolution: 1080, + }), + revision: Some(Revision { + version: 1, + real: 0, + is_repack: false, + }), + }; + + assert!(quality.quality.is_some()); + assert!(quality.revision.is_some()); + assert_eq!(quality.quality.as_ref().unwrap().resolution, 1080); + } + + #[test] + fn test_release_filtering_logic() { + let releases = vec![ + create_test_release(1, "Low Quality", 500, 0, true, false, 1_000_000_000), + create_test_release(2, "High Quality", 1500, 200, true, false, 3_000_000_000), + create_test_release(3, "Rejected", 2000, 300, true, true, 2_000_000_000), + create_test_release(4, "Not Allowed", 1800, 150, false, false, 2_500_000_000), + create_test_release(5, "Best Quality", 2000, 500, true, false, 4_000_000_000), + ]; + + // Filter downloadable releases + let downloadable: Vec<_> = releases + .iter() + .filter(|r| r.download_allowed.unwrap_or(false) && !r.rejected.unwrap_or(true)) + .collect(); + + assert_eq!(downloadable.len(), 3); // Should exclude rejected and not allowed + + // Sort by quality weight and custom format score (simulate best quality selection) + let mut sorted_releases = downloadable.clone(); + sorted_releases.sort_by(|a, b| { + let quality_cmp = b + .quality_weight + .unwrap_or(0) + .cmp(&a.quality_weight.unwrap_or(0)); + if quality_cmp == std::cmp::Ordering::Equal { + b.custom_format_score + .unwrap_or(0) + .cmp(&a.custom_format_score.unwrap_or(0)) + } else { + quality_cmp + } + }); + + // Best release should be "Best Quality" (highest quality_weight + custom_format_score) + assert_eq!(sorted_releases[0].title, Some("Best Quality".to_string())); + } + + #[test] + fn test_search_endpoint_building() { + // Test endpoint building logic (simulates what would happen in search_releases) + let mut query_params = Vec::new(); + + let series_id = Some(123u32); + let episode_id = Some(456u32); + let season_number = Some(2i32); + + if let Some(id) = series_id { + query_params.push(format!("seriesId={}", id)); + } + if let Some(id) = episode_id { + query_params.push(format!("episodeId={}", id)); + } + if let Some(season) = season_number { + query_params.push(format!("seasonNumber={}", season)); + } + + let endpoint = if query_params.is_empty() { + "/release".to_string() + } else { + format!("/release?{}", query_params.join("&")) + }; + + assert_eq!( + endpoint, + "/release?seriesId=123&episodeId=456&seasonNumber=2" + ); + } + + #[test] + fn test_empty_search_parameters() { + let query_params: Vec = Vec::new(); + let endpoint = if query_params.is_empty() { + "/release".to_string() + } else { + format!("/release?{}", query_params.join("&")) + }; + + assert_eq!(endpoint, "/release"); + } + + #[test] + fn test_release_size_handling() { + let release = create_test_release(1, "Big Release", 1000, 100, true, false, 15_000_000_000); // 15GB + + // Test size filtering logic + let is_too_large = release.size.unwrap_or(0) > 10 * 1024 * 1024 * 1024; // > 10GB + assert!(is_too_large); + + let small_release = + create_test_release(2, "Small Release", 1000, 100, true, false, 2_000_000_000); // 2GB + let is_acceptable_size = small_release.size.unwrap_or(0) <= 10 * 1024 * 1024 * 1024; // <= 10GB + assert!(is_acceptable_size); + } + + #[test] + fn test_seeder_filtering() { + let low_seed_release = + create_test_release(1, "Low Seeds", 1000, 100, true, false, 2_000_000_000); + let mut test_release = low_seed_release; + test_release.seeders = Some(2); + + let has_enough_seeders = test_release.seeders.unwrap_or(0) >= 5; + assert!(!has_enough_seeders); + + test_release.seeders = Some(10); + let has_enough_seeders = test_release.seeders.unwrap_or(0) >= 5; + assert!(has_enough_seeders); + } + + #[test] + fn test_custom_format_score_comparison() { + let release_a = create_test_release(1, "Release A", 1000, 100, true, false, 2_000_000_000); + let release_b = create_test_release(2, "Release B", 1000, 200, true, false, 2_000_000_000); + + // Same quality weight, but B has higher custom format score + let score_a = release_a.custom_format_score.unwrap_or(0); + let score_b = release_b.custom_format_score.unwrap_or(0); + + assert!(score_b > score_a); + } + + #[test] + fn test_release_rejection_reasons() { + let rejected_release = + create_test_release(1, "Rejected", 1000, 100, true, true, 2_000_000_000); + + assert!(rejected_release.rejected.unwrap_or(false)); + assert!(rejected_release.rejections.is_some()); + assert!(!rejected_release.rejections.as_ref().unwrap().is_empty()); + } + + #[test] + fn test_language_handling() { + let release = create_test_release(1, "Test", 1000, 100, true, false, 2_000_000_000); + + assert!(release.languages.is_some()); + let languages = release.languages.unwrap(); + assert_eq!(languages.len(), 1); + assert_eq!(languages[0].name, Some("English".to_string())); + } + + #[test] + fn test_episode_number_handling() { + let release = create_test_release(1, "Test", 1000, 100, true, false, 2_000_000_000); + + assert!(release.episode_numbers.is_some()); + let episodes = release.episode_numbers.unwrap(); + assert_eq!(episodes, vec![1]); + } + + // Helper function tests + #[test] + fn test_format_size_helper() { + fn format_size(bytes: i64) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; + + if bytes == 0 { + return "0 B".to_string(); + } + + let mut size = bytes as f64; + let mut unit_index = 0; + + while size >= 1024.0 && unit_index < UNITS.len() - 1 { + size /= 1024.0; + unit_index += 1; + } + + format!("{:.1} {}", size, UNITS[unit_index]) + } + + assert_eq!(format_size(0), "0 B"); + assert_eq!(format_size(1024), "1.0 KB"); + assert_eq!(format_size(1536), "1.5 KB"); + assert_eq!(format_size(1048576), "1.0 MB"); + assert_eq!(format_size(1073741824), "1.0 GB"); + assert_eq!(format_size(2_000_000_000), "1.9 GB"); + } +}