diff --git a/.env.example b/.env.example index ff114a1..876ba2f 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,36 @@ -# Navidrome/Subsonic server URL +# ===== BACKEND SELECTION ===== +# Choose which media server backend to use: Subsonic or Jellyfin +BACKEND_TYPE=Subsonic + +# ===== REDIS CACHE ===== +# Enable Redis caching for metadata and images (default: true) +REDIS_ENABLED=true + +# ===== SUBSONIC/NAVIDROME CONFIGURATION ===== +# Server URL (required if using Subsonic backend) SUBSONIC_URL=http://localhost:4533 +# ===== JELLYFIN CONFIGURATION ===== +# Server URL (required if using Jellyfin backend) +JELLYFIN_URL=http://localhost:8096 + +# API key for authentication (get from Jellyfin Dashboard > API Keys) +JELLYFIN_API_KEY= + +# User ID (get from Jellyfin Dashboard > Users > click user > check URL) +JELLYFIN_USER_ID= + +# Music library ID (optional, auto-detected if not set) +JELLYFIN_LIBRARY_ID= + +# ===== MUSIC SOURCE SELECTION ===== +# Music service to use: SquidWTF, Deezer, or Qobuz (default: SquidWTF) +MUSIC_SERVICE=SquidWTF + # Path where downloaded songs will be stored on the host (only applies if STORAGE_MODE=Permanent) DOWNLOAD_PATH=./downloads -# Music service to use: SquidWTF, Deezer, or Qobuz (default: SquidWTF) -MUSIC_SERVICE=SquidWTF - -# ===== SquidWTF CONFIGURATION ===== +# ===== SQUIDWTF CONFIGURATION ===== # Different quality options for SquidWTF. Only FLAC supported right now SQUIDWTF_QUALITY=FLAC @@ -61,14 +84,14 @@ EXPLICIT_FILTER=All DOWNLOAD_MODE=Track # Storage mode (optional, default: Permanent) -# - Permanent: Files are saved to the library permanently and registered in Navidrome +# - Permanent: Files are saved to the library permanently and registered in the media server # - Cache: Files are stored in /tmp and automatically cleaned up after CACHE_DURATION_HOURS -# Not registered in Navidrome, ideal for streaming without library bloat +# Not registered in media server, ideal for streaming without library bloat # Note: On Linux/Docker, you can customize cache location by setting TMPDIR environment variable STORAGE_MODE=Permanent # Cache duration in hours (optional, default: 1) # Files older than this duration will be automatically deleted when STORAGE_MODE=Cache # Based on last access time (updated each time the file is streamed) -# Cache location: /tmp/octo-fiesta-cache (or $TMPDIR/octo-fiesta-cache if TMPDIR is set) +# Cache location: /tmp/allstarr-cache (or $TMPDIR/allstarr-cache if TMPDIR is set) CACHE_DURATION_HOURS=1 diff --git a/.gitignore b/.gitignore index 81b563b..a1cf899 100644 --- a/.gitignore +++ b/.gitignore @@ -68,7 +68,19 @@ obj/ # Autres fichiers temporaires *.log -/.env - -# Downloaded music files -octo-fiesta/downloads/ \ No newline at end of file +/.env + +# Downloaded music files +downloads/ +!downloads/.gitkeep + +# Docker volumes +redis-data/ + +# API keys and specs (ignore markdown docs, keep OpenAPI spec) +apis/*.md +apis/*.json +!apis/jellyfin-openapi-stable.json + +# Original source code for reference +originals/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f49119c..e8b5697 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,21 +2,24 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src -COPY octo-fiesta.sln . -COPY octo-fiesta/octo-fiesta.csproj octo-fiesta/ -COPY octo-fiesta.Tests/octo-fiesta.Tests.csproj octo-fiesta.Tests/ +COPY allstarr.sln . +COPY allstarr/allstarr.csproj allstarr/ +COPY allstarr.Tests/allstarr.Tests.csproj allstarr.Tests/ RUN dotnet restore -COPY octo-fiesta/ octo-fiesta/ -COPY octo-fiesta.Tests/ octo-fiesta.Tests/ +COPY allstarr/ allstarr/ +COPY allstarr.Tests/ allstarr.Tests/ -RUN dotnet publish octo-fiesta/octo-fiesta.csproj -c Release -o /app/publish +RUN dotnet publish allstarr/allstarr.csproj -c Release -o /app/publish # Runtime stage FROM mcr.microsoft.com/dotnet/aspnet:9.0 WORKDIR /app +# Install curl for health checks +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + RUN mkdir -p /app/downloads COPY --from=build /app/publish . @@ -24,4 +27,4 @@ COPY --from=build /app/publish . EXPOSE 8080 ENV ASPNETCORE_URLS=http://+:8080 -ENTRYPOINT ["dotnet", "octo-fiesta.dll"] +ENTRYPOINT ["dotnet", "allstarr.dll"] diff --git a/README.md b/README.md index 93dce47..f85821b 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,107 @@ -# Octo-Fiestarr +# Allstarr -A Subsonic API proxy server that transparently integrates multiple music streaming providers as sources. When a song is not available in your local Navidrome library, it is automatically fetched from your configured provider, downloaded, and served to your Subsonic-compatible client. The downloaded song is then added to your library, making it available locally for future listens. +A media server proxy that integrates music streaming providers with your local library. Works with **Jellyfin** and **Subsonic-compatible** servers (Navidrome). When a song isn't in your local library, it gets fetched from your configured provider, downloaded, and served to your client. The downloaded song then lives in your library for next time. -## Why "Octo-Fiestarr"? +**THIS IS UNDER ACTIVE DEVELOPMENT** -This fork was created to focus on integrating the original concept of Octo-Fiesta with music providers that do not require API credentials, such as SquidWTF. This allows for seamless external music discovery without the need for any subscriptions. Thus, I saw it fitting to change the name of the fork to resemble other *arr projects. +Please report all bugs as soon as possible, as the Jellyfin addition is entirely a test at this point + +## Quick Start + +```bash +# 1. Configure environment +cp .env.example .env +nano .env # Edit with your settings + +# 2. Start services +docker-compose up -d --build + +# 3. Check status +docker-compose ps +docker-compose logs -f +``` + +### Nginx Proxy Setup (Required) + +This service only exposes ports internally. You **must** use nginx to proxy to it: + +```nginx +server { + listen 443 ssl http2; + server_name your-domain.com; + + ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + + # Security headers + add_header Strict-Transport-Security "max-age=31536000" always; + add_header X-Content-Type-Options "nosniff" always; + + # Streaming settings + proxy_buffering off; + proxy_request_buffering off; + proxy_read_timeout 600s; + + location / { + proxy_pass http://allstarr:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +**Security:** All authentication is forwarded to Jellyfin - this is as secure as Jellyfin itself. Always use HTTPS for public access. + +## Why "Allstarr"? + +This project brings together all the music streaming providers into one unified library - making them all stars in your collection. ## Features -- **Multi-Provider Architecture**: Pluggable music service system supporting multiple streaming providers (Deezer, Qobuz, and more to come) -- **Transparent Proxy**: Acts as a middleware between Subsonic clients (like Aonsoku, Sublime Music, etc.) and your Navidrome server -- **Seamless Integration**: Automatically searches and streams music from your configured provider when not available locally -- **Automatic Downloads**: Songs are downloaded on-the-fly and cached for future use -- **External Playlist Support**: Search, discover, and download playlists from Deezer, Qobuz, and SquidWTF with automatic M3U generation -- **Hi-Res Audio Support**: SquidWTF provider supports up to 24-bit/192kHz FLAC quality -- **Full Metadata Embedding**: Downloaded files include complete ID3 tags (title, artist, album, track number, year, genre, BPM, ISRC, etc.) and embedded cover art -- **Organized Library**: Downloads are saved in a clean `Artist/Album/Track` folder structure -- **Artist Deduplication**: Merges local and streaming provider artists to avoid duplicates in search results -- **Album Enrichment**: Local albums are enriched with missing tracks from streaming providers -- **Cover Art Proxy**: Serves cover art for external content transparently +- **Dual Backend Support**: Works with Jellyfin and Subsonic-compatible servers (Navidrome, Airsonic, etc.) +- **Multi-Provider Architecture**: Pluggable system for streaming providers (Deezer, Qobuz, SquidWTF) +- **Transparent Proxy**: Sits between your music clients and media server +- **Automatic Search**: Searches streaming providers when songs aren't local +- **On-the-Fly Downloads**: Songs download and cache for future use +- **External Playlist Support**: Search and download playlists from Deezer, Qobuz, and SquidWTF with M3U generation +- **Hi-Res Audio**: SquidWTF supports up to 24-bit/192kHz FLAC +- **Full Metadata**: Downloaded files include complete ID3 tags (title, artist, album, track number, year, genre, BPM, ISRC, etc.) and cover art +- **Organized Library**: Downloads save in `Artist/Album/Track` folder structure +- **Artist Deduplication**: Merges local and streaming artists to avoid duplicates +- **Album Enrichment**: Adds missing tracks to local albums from streaming providers +- **Cover Art Proxy**: Serves cover art for external content -## Compatible Clients +## Supported Backends -### PC +### Jellyfin +[Jellyfin](https://jellyfin.org/) is a free and open-source media server. Allstarr connects via the Jellyfin API using your Jellyfin user login. (I plan to move this to api key if possible) +**Compatible Jellyfin clients:** + +- [Feishin](https://github.com/jeffvli/feishin) (Mac/Windows/Linux) +- [Musiver](https://music.aqzscn.cn/en/) (Android/IOS/Windows/Android) + +_Working on getting more currently_ + +### Subsonic/Navidrome +[Navidrome](https://www.navidrome.org/) and other Subsonic-compatible servers are supported via the Subsonic API. + +**Compatible Subsonic clients:** + +#### PC - [Aonsoku](https://github.com/victoralvesf/aonsoku) - [Feishin](https://github.com/jeffvli/feishin) - [Subplayer](https://github.com/peguerosdc/subplayer) - [Aurial](https://github.com/shrimpza/aurial) -### Android - +#### Android - [Tempus](https://github.com/eddyizm/tempus) - [Substreamer](https://substreamerapp.com/) -### iOS - +#### iOS - [Narjo](https://www.reddit.com/r/NarjoApp/) - [Arpeggi](https://www.reddit.com/r/arpeggiApp/) @@ -43,7 +109,7 @@ This fork was created to focus on integrating the original concept of Octo-Fiest ### Incompatible Clients -These clients are **not compatible** with octo-fiesta due to architectural limitations: +These clients are **not compatible** with Allstarr due to architectural limitations: - [Symfonium](https://symfonium.app/) - Uses offline-first architecture and never queries the server for searches, making streaming provider integration impossible. [See details](https://support.symfonium.app/t/suggestions-on-search-function/1121/) @@ -57,15 +123,17 @@ Choose your preferred provider via the `MUSIC_SERVICE` environment variable. Add ## Requirements -- A running Subsonic-compatible server (developed and tested with [Navidrome](https://www.navidrome.org/)) +- A running media server: + - **Jellyfin**: Any recent version with API access enabled + - **Subsonic**: Navidrome or other Subsonic-compatible server - Credentials for at least one music provider (IF NOT USING SQUIDWTF): - **Deezer**: ARL token from browser cookies - - **Qobuz**: User ID + User Auth Token from browser localStorage ([see Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token))) + - **Qobuz**: User ID + User Auth Token from browser localStorage ([see Wiki guide](https://github.com/V1ck3s/allstarr/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token))) - Docker and Docker Compose (recommended) **or** [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) for manual installation ## Quick Start (Docker) -The easiest way to run Octo-Fiestarr is with Docker Compose. +The easiest way to run Allstarr is with Docker Compose. 1. **Create your environment file** ```bash @@ -73,82 +141,47 @@ The easiest way to run Octo-Fiestarr is with Docker Compose. ``` 2. **Edit the `.env` file** with your configuration: + + **For Jellyfin backend:** ```bash - # Navidrome/Subsonic server URL - SUBSONIC_URL=http://localhost:4533 - - # Path where downloaded songs will be stored on the host (only applies if STORAGE_MODE=Permanent) - DOWNLOAD_PATH=./downloads - - # Music service to use: SquidWTF, Deezer, or Qobuz (default: SquidWTF) - MUSIC_SERVICE=SquidWTF - - # ===== SquidWTF CONFIGURATION ===== - # Different quality options for SquidWTF. Only FLAC supported right now - SQUIDWTF_QUALITY=FLAC - - # ===== DEEZER CONFIGURATION ===== - # Deezer ARL token (required if using Deezer) - # See README.md for instructions on how to get this token - DEEZER_ARL=your-deezer-arl-token - - # Fallback ARL token (optional) - DEEZER_ARL_FALLBACK= - - # Preferred audio quality: FLAC, MP3_320, MP3_128 (optional) - # If not specified, the highest available quality for your account will be used - DEEZER_QUALITY= - - # ===== QOBUZ CONFIGURATION ===== - # Qobuz user authentication token (required if using Qobuz) - # Get this from your browser after logging into play.qobuz.com - # See README.md for detailed instructions - QOBUZ_USER_AUTH_TOKEN= - - # Qobuz user ID (required if using Qobuz) - # Get this from your browser after logging into play.qobuz.com - QOBUZ_USER_ID= - - # Preferred audio quality: FLAC, FLAC_24_HIGH, FLAC_24_LOW, FLAC_16, MP3_320 (optional) - # If not specified, the highest available quality will be used - QOBUZ_QUALITY= - - # ===== GENERAL SETTINGS ===== - # External playlists support (optional, default: true) - # When enabled, allows searching and downloading playlists from Deezer/Qobuz - # Starring a playlist triggers automatic download of all tracks and creates an M3U file - ENABLE_EXTERNAL_PLAYLISTS=true - - # Playlists directory name (optional, default: playlists) - # M3U playlist files will be created in {DOWNLOAD_PATH}/{PLAYLISTS_DIRECTORY}/ - PLAYLISTS_DIRECTORY=playlists - - # Explicit content filter (optional, default: All) - # - All: Show all tracks (no filtering) - # - ExplicitOnly: Exclude clean/edited versions, keep original explicit content - # - CleanOnly: Only show clean content (naturally clean or edited versions) - # Note: This only works with Deezer, Qobuz doesn't expose explicit content flags - EXPLICIT_FILTER=All - - # Download mode (optional, default: Track) - # - Track: Download only the played track - # - Album: When playing a track, download the entire album in background - # The played track is downloaded first, remaining tracks are queued - DOWNLOAD_MODE=Track - - # Storage mode (optional, default: Permanent) - # - Permanent: Files are saved to the library permanently and registered in Navidrome - # - Cache: Files are stored in /tmp and automatically cleaned up after CACHE_DURATION_HOURS - # Not registered in Navidrome, ideal for streaming without library bloat - # Note: On Linux/Docker, you can customize cache location by setting TMPDIR environment variable - STORAGE_MODE=Permanent - - # Cache duration in hours (optional, default: 1) - # Files older than this duration will be automatically deleted when STORAGE_MODE=Cache - # Based on last access time (updated each time the file is streamed) - # Cache location: /tmp/octo-fiesta-cache (or $TMPDIR/octo-fiesta-cache if TMPDIR is set) - CACHE_DURATION_HOURS=1 - ``` + # Backend selection + BACKEND_TYPE=Jellyfin + + # Jellyfin server URL + JELLYFIN_URL=http://localhost:8096 + + # API key (get from Jellyfin Dashboard > API Keys) + JELLYFIN_API_KEY=your-api-key-here + + # User ID (from Jellyfin Dashboard > Users > click user > check URL) + JELLYFIN_USER_ID=your-user-id-here + + # Music library ID (optional, auto-detected if not set) + JELLYFIN_LIBRARY_ID= + ``` + + **For Subsonic/Navidrome backend:** + ```bash + # Backend selection + BACKEND_TYPE=Subsonic + + # Navidrome/Subsonic server URL + SUBSONIC_URL=http://localhost:4533 + ``` + + **Common settings (both backends):** + ```bash + # Path where downloaded songs will be stored + DOWNLOAD_PATH=./downloads + + # Music service to use: SquidWTF, Deezer, or Qobuz + MUSIC_SERVICE=SquidWTF + + # Storage mode: Permanent or Cache + STORAGE_MODE=Permanent + ``` + + See the full `.env.example` for all available options including Deezer/Qobuz credentials. 3. **Start the container** ```bash @@ -157,21 +190,47 @@ The easiest way to run Octo-Fiestarr is with Docker Compose. The proxy will be available at `http://localhost:5274`. -4. **Configure your Subsonic client** +4. **Configure your client** - Point your Subsonic client to `http://localhost:5274` instead of your Navidrome server directly. + Point your music client to `http://localhost:5274` instead of your media server directly. -> **Tip**: Make sure the `DOWNLOAD_PATH` points to a directory that Navidrome can scan, so downloaded songs appear in your library. +> **Tip**: Make sure the `DOWNLOAD_PATH` points to a directory that your media server can scan, so downloaded songs appear in your library. ## Configuration -### General Settings +### Backend Selection + +| Setting | Description | +|---------|-------------| +| `Backend:Type` | Backend type: `Subsonic` or `Jellyfin` (default: `Subsonic`) | + +### Jellyfin Settings + +| Setting | Description | +|---------|-------------| +| `Jellyfin:Url` | URL of your Jellyfin server | +| `Jellyfin:ApiKey` | API key (get from Jellyfin Dashboard > API Keys) | +| `Jellyfin:UserId` | User ID for library access | +| `Jellyfin:LibraryId` | Music library ID (optional, auto-detected) | +| `Jellyfin:MusicService` | Music provider: `SquidWTF`, `Deezer`, or `Qobuz` | + +### Subsonic Settings | Setting | Description | |---------|-------------| | `Subsonic:Url` | URL of your Navidrome/Subsonic server | -| `Subsonic:MusicService` | Music provider to use: `SquidWTF`, `Deezer`, or `Qobuz` (default: `SquidWTF`) | +| `Subsonic:MusicService` | Music provider: `SquidWTF`, `Deezer`, or `Qobuz` (default: `SquidWTF`) | + +### Shared Settings + +| Setting | Description | +|---------|-------------| | `Library:DownloadPath` | Directory where downloaded songs are stored | +| `*:ExplicitFilter` | Content filter: `All`, `ExplicitOnly`, or `CleanOnly` | +| `*:DownloadMode` | Download mode: `Track` or `Album` | +| `*:StorageMode` | Storage mode: `Permanent` or `Cache` | +| `*:CacheDurationHours` | Cache expiration time in hours | +| `*:EnableExternalPlaylists` | Enable external playlist support | ### SquidWTF Settings @@ -191,13 +250,13 @@ The easiest way to run Octo-Fiestarr is with Docker Compose. | Setting | Description | |---------|-------------| -| `Qobuz:UserAuthToken` | Your Qobuz User Auth Token (required if using Qobuz) - [How to get it](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token)) | +| `Qobuz:UserAuthToken` | Your Qobuz User Auth Token (required if using Qobuz) - [How to get it](https://github.com/V1ck3s/allstarr/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token)) | | `Qobuz:UserId` | Your Qobuz User ID (required if using Qobuz) | | `Qobuz:Quality` | Preferred audio quality: `FLAC`, `FLAC_24_HIGH`, `FLAC_24_LOW`, `FLAC_16`, `MP3_320`. If not specified, the highest available quality will be used | ### External Playlists -Octo-Fiesta supports discovering and downloading playlists from your streaming providers (SquidWTF, Deezer, and Qobuz). +Allstarr supports discovering and downloading playlists from your streaming providers (SquidWTF, Deezer, and Qobuz). | Setting | Description | |---------|-------------| @@ -206,7 +265,7 @@ Octo-Fiesta supports discovering and downloading playlists from your streaming p **How it works:** 1. Search for playlists from an external provider using the global search in your Subsonic client -2. When you "star" (favorite) a playlist, Octo-Fiesta automatically downloads all tracks +2. When you "star" (favorite) a playlist, Allstarr automatically downloads all tracks 3. An M3U playlist file is created in `{DownloadPath}/playlists/` with relative paths to downloaded tracks 4. Individual tracks are added to the M3U as they are played or downloaded @@ -222,11 +281,11 @@ Subsonic__EnableExternalPlaylists=false #### Deezer ARL Token -See the [Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Deezer-Credentials-(ARL-Token)) for detailed instructions on obtaining your Deezer ARL token. +See the [Wiki guide](https://github.com/V1ck3s/allstarr/wiki/Getting-Deezer-Credentials-(ARL-Token)) for detailed instructions on obtaining your Deezer ARL token. #### Qobuz Credentials -See the [Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token)) for detailed instructions on obtaining your Qobuz User ID and User Auth Token. +See the [Wiki guide](https://github.com/V1ck3s/allstarr/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token)) for detailed instructions on obtaining your Qobuz User ID and User Auth Token. ## Limitations @@ -237,29 +296,39 @@ See the [Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Cr ## Architecture ``` -┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ Subsonic │────▶│ Octo-Fiesta │────▶│ Navidrome │ -│ Client │◀────│ (Proxy) │◀────│ Server │ -│ (Aonsoku) │ │ │ │ │ -└─────────────────┘ └────────┬─────────┘ └─────────────────┘ - │ - ▼ + ┌─────────────────┐ + ┌───▶│ Jellyfin │ +┌─────────────────┐ ┌──────────────────┐ │ │ Server │ +│ Music Client │────▶│ Allstarr │───┤ └─────────────────┘ +│ (Aonsoku, │◀────│ (Proxy) │◀──┤ +│ Finamp, etc.) │ │ │ │ ┌─────────────────┐ +└─────────────────┘ └────────┬─────────┘ └───▶│ Navidrome │ + │ │ (Subsonic) │ + ▼ └─────────────────┘ ┌─────────────────┐ │ Music Providers │ + │ - SquidWTF │ │ - Deezer │ │ - Qobuz │ - │ - (more...) │ └─────────────────┘ ``` +The proxy intercepts requests from your music client and: +1. Forwards library requests to your configured backend (Jellyfin or Subsonic) +2. Merges results with content from your music provider +3. Downloads and caches external tracks on-demand +4. Serves audio streams transparently + +**Note**: Only the controller matching your configured `BACKEND_TYPE` is registered at runtime, preventing route conflicts and ensuring clean API separation. + ## Manual Installation -If you prefer to run Octo-Fiesta without Docker: +If you prefer to run Allstarr without Docker: 1. **Clone the repository** ```bash - git clone https://github.com/your-username/octo-fiesta.git - cd octo-fiesta + git clone https://github.com/your-username/allstarr.git + cd allstarr ``` 2. **Restore dependencies** @@ -269,55 +338,63 @@ If you prefer to run Octo-Fiesta without Docker: 3. **Configure the application** - Edit `octo-fiesta/appsettings.json`: + Edit `allstarr/appsettings.json`: + + **For Jellyfin:** ```json -{ - "Subsonic": { - "Url": "https://navidrome.local.bransonb.com", - "MusicService": "SquidWTF", - "ExplicitFilter": "All", - "DownloadMode": "Track", - "StorageMode": "Permanent", - "CacheDurationHours": 1 - }, - "Library": { - "DownloadPath": "./downloads" - }, - "Qobuz": { - "UserAuthToken": "your-qobuz-token", - "UserId": "your-qobuz-user-id", - "Quality": "FLAC" - }, - "Deezer": { - "Arl": "your-deezer-arl-token", - "ArlFallback": "", - "Quality": "FLAC" - }, - "SquidWTF": { - "Quality": "FLAC" - } -} -``` + { + "Backend": { + "Type": "Jellyfin" + }, + "Jellyfin": { + "Url": "http://localhost:8096", + "ApiKey": "your-api-key", + "UserId": "your-user-id", + "MusicService": "SquidWTF" + }, + "Library": { + "DownloadPath": "./downloads" + } + } + ``` + + **For Subsonic/Navidrome:** + ```json + { + "Backend": { + "Type": "Subsonic" + }, + "Subsonic": { + "Url": "http://localhost:4533", + "MusicService": "SquidWTF" + }, + "Library": { + "DownloadPath": "./downloads" + } + } + ``` 4. **Run the server** ```bash - cd octo-fiesta + cd allstarr dotnet run ``` The proxy will start on `http://localhost:5274` by default. -5. **Configure your Subsonic client** +5. **Configure your client** - Point your Subsonic client to `http://localhost:5274` instead of your Navidrome server directly. + Point your music client to `http://localhost:5274` instead of your media server directly. ## API Endpoints -The proxy implements the Subsonic API and adds transparent streaming provider integration to: +### Subsonic Backend + +The proxy implements the Subsonic API and adds transparent streaming provider integration: | Endpoint | Description | |----------|-------------| -| `GET /rest/search3` | Merged search results from Navidrome + streaming provider (including playlists) | +| `GET /rest/search3` | Merged search results from Navidrome + streaming provider | | `GET /rest/stream` | Streams audio, downloading from provider if needed | | `GET /rest/getSong` | Returns song details (local or from provider) | | `GET /rest/getAlbum` | Returns album with tracks from both sources | @@ -327,6 +404,20 @@ The proxy implements the Subsonic API and adds transparent streaming provider in All other Subsonic API endpoints are passed through to Navidrome unchanged. +### Jellyfin Backend + +The proxy implements a subset of the Jellyfin API: + +| Endpoint | Description | +|----------|-------------| +| `GET /Items` | Search and browse library items | +| `GET /Artists` | Browse artists with streaming provider results | +| `GET /Audio/{id}/stream` | Stream audio, downloading from provider if needed | +| `GET /Items/{id}/Images/{type}` | Proxy cover art for external content | +| `POST /UserFavoriteItems/{id}` | Favorite items; triggers playlist download | + +All other Jellyfin API endpoints are passed through unchanged. + ## External ID Format External (streaming provider) content uses typed IDs: @@ -388,9 +479,10 @@ dotnet test ### Project Structure ``` -octo-fiesta/ +allstarr/ ├── Controllers/ -│ └── SubsonicController.cs # Main API controller +│ ├── JellyfinController.cs # Jellyfin API controller (registered when Backend:Type=Jellyfin) +│ └── SubsonicController.cs # Subsonic API controller (registered when Backend:Type=Subsonic) ├── Middleware/ │ └── GlobalExceptionHandler.cs # Global error handling ├── Models/ @@ -444,7 +536,7 @@ octo-fiesta/ ├── Program.cs # Application entry point └── appsettings.json # Configuration -octo-fiesta.Tests/ +allstarr.Tests/ ├── DeezerDownloadServiceTests.cs # Deezer download tests ├── DeezerMetadataServiceTests.cs # Deezer metadata tests ├── QobuzDownloadServiceTests.cs # Qobuz download tests (127 tests) diff --git a/octo-fiesta.Tests/DeezerDownloadServiceTests.cs b/allstarr.Tests/DeezerDownloadServiceTests.cs similarity index 95% rename from octo-fiesta.Tests/DeezerDownloadServiceTests.cs rename to allstarr.Tests/DeezerDownloadServiceTests.cs index d66cc92..f0fa1a2 100644 --- a/octo-fiesta.Tests/DeezerDownloadServiceTests.cs +++ b/allstarr.Tests/DeezerDownloadServiceTests.cs @@ -1,12 +1,12 @@ -using octo_fiesta.Services; -using octo_fiesta.Services.Deezer; -using octo_fiesta.Services.Local; -using octo_fiesta.Services.Common; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; +using allstarr.Services; +using allstarr.Services.Deezer; +using allstarr.Services.Local; +using allstarr.Services.Common; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -15,7 +15,7 @@ using Moq.Protected; using System.Net; using System.Text.Json; -namespace octo_fiesta.Tests; +namespace allstarr.Tests; public class DeezerDownloadServiceTests : IDisposable { @@ -29,7 +29,7 @@ public class DeezerDownloadServiceTests : IDisposable public DeezerDownloadServiceTests() { - _testDownloadPath = Path.Combine(Path.GetTempPath(), "octo-fiesta-download-tests-" + Guid.NewGuid()); + _testDownloadPath = Path.Combine(Path.GetTempPath(), "allstarr-download-tests-" + Guid.NewGuid()); Directory.CreateDirectory(_testDownloadPath); _httpMessageHandlerMock = new Mock(); @@ -84,7 +84,7 @@ public class DeezerDownloadServiceTests : IDisposable }); var serviceProviderMock = new Mock(); - serviceProviderMock.Setup(sp => sp.GetService(typeof(octo_fiesta.Services.Subsonic.PlaylistSyncService))) + serviceProviderMock.Setup(sp => sp.GetService(typeof(allstarr.Services.Subsonic.PlaylistSyncService))) .Returns(null); return new DeezerDownloadService( @@ -235,7 +235,7 @@ public class PathHelperTests : IDisposable public PathHelperTests() { - _testPath = Path.Combine(Path.GetTempPath(), "octo-fiesta-pathhelper-tests-" + Guid.NewGuid()); + _testPath = Path.Combine(Path.GetTempPath(), "allstarr-pathhelper-tests-" + Guid.NewGuid()); Directory.CreateDirectory(_testPath); } diff --git a/octo-fiesta.Tests/DeezerMetadataServiceTests.cs b/allstarr.Tests/DeezerMetadataServiceTests.cs similarity index 99% rename from octo-fiesta.Tests/DeezerMetadataServiceTests.cs rename to allstarr.Tests/DeezerMetadataServiceTests.cs index 1b55a56..222164c 100644 --- a/octo-fiesta.Tests/DeezerMetadataServiceTests.cs +++ b/allstarr.Tests/DeezerMetadataServiceTests.cs @@ -1,16 +1,16 @@ -using octo_fiesta.Services.Deezer; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; +using allstarr.Services.Deezer; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; using Moq; using Moq.Protected; using Microsoft.Extensions.Options; using System.Net; using System.Text.Json; -namespace octo_fiesta.Tests; +namespace allstarr.Tests; public class DeezerMetadataServiceTests { diff --git a/allstarr.Tests/JellyfinModelMapperTests.cs b/allstarr.Tests/JellyfinModelMapperTests.cs new file mode 100644 index 0000000..4fdf9c5 --- /dev/null +++ b/allstarr.Tests/JellyfinModelMapperTests.cs @@ -0,0 +1,401 @@ +using Microsoft.Extensions.Logging; +using Moq; +using allstarr.Models.Domain; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; +using allstarr.Services.Jellyfin; +using System.Text.Json; + +namespace allstarr.Tests; + +public class JellyfinModelMapperTests +{ + private readonly JellyfinModelMapper _mapper; + private readonly JellyfinResponseBuilder _responseBuilder; + + public JellyfinModelMapperTests() + { + _responseBuilder = new JellyfinResponseBuilder(); + var mockLogger = new Mock>(); + _mapper = new JellyfinModelMapper(_responseBuilder, mockLogger.Object); + } + + [Fact] + public void ParseItemsResponse_AudioItems_ReturnsSongs() + { + // Arrange + var json = @"{ + ""Items"": [ + { + ""Id"": ""song-abc"", + ""Name"": ""Test Song"", + ""Type"": ""Audio"", + ""Album"": ""Test Album"", + ""AlbumId"": ""album-123"", + ""RunTimeTicks"": 2450000000, + ""IndexNumber"": 5, + ""ParentIndexNumber"": 1, + ""ProductionYear"": 2022, + ""Artists"": [""Test Artist""], + ""Genres"": [""Rock""] + } + ], + ""TotalRecordCount"": 1 + }"; + var doc = JsonDocument.Parse(json); + + // Act + var (songs, albums, artists) = _mapper.ParseItemsResponse(doc); + + // Assert + Assert.Single(songs); + Assert.Empty(albums); + Assert.Empty(artists); + + var song = songs[0]; + Assert.Equal("song-abc", song.Id); + Assert.Equal("Test Song", song.Title); + Assert.Equal("Test Album", song.Album); + Assert.Equal("Test Artist", song.Artist); + Assert.Equal(245, song.Duration); // 2450000000 ticks = 245 seconds + Assert.Equal(5, song.Track); + Assert.Equal(1, song.DiscNumber); + Assert.Equal(2022, song.Year); + Assert.Equal("Rock", song.Genre); + } + + [Fact] + public void ParseItemsResponse_AlbumItems_ReturnsAlbums() + { + // Arrange + var json = @"{ + ""Items"": [ + { + ""Id"": ""album-xyz"", + ""Name"": ""Greatest Hits"", + ""Type"": ""MusicAlbum"", + ""AlbumArtist"": ""Famous Band"", + ""ProductionYear"": 2020, + ""ChildCount"": 14, + ""Genres"": [""Pop""], + ""AlbumArtists"": [{""Id"": ""artist-1"", ""Name"": ""Famous Band""}] + } + ] + }"; + var doc = JsonDocument.Parse(json); + + // Act + var (songs, albums, artists) = _mapper.ParseItemsResponse(doc); + + // Assert + Assert.Empty(songs); + Assert.Single(albums); + Assert.Empty(artists); + + var album = albums[0]; + Assert.Equal("album-xyz", album.Id); + Assert.Equal("Greatest Hits", album.Title); + Assert.Equal("Famous Band", album.Artist); + Assert.Equal(2020, album.Year); + Assert.Equal(14, album.SongCount); + Assert.Equal("Pop", album.Genre); + } + + [Fact] + public void ParseItemsResponse_ArtistItems_ReturnsArtists() + { + // Arrange + var json = @"{ + ""Items"": [ + { + ""Id"": ""artist-999"", + ""Name"": ""The Rockers"", + ""Type"": ""MusicArtist"", + ""AlbumCount"": 7 + } + ] + }"; + var doc = JsonDocument.Parse(json); + + // Act + var (songs, albums, artists) = _mapper.ParseItemsResponse(doc); + + // Assert + Assert.Empty(songs); + Assert.Empty(albums); + Assert.Single(artists); + + var artist = artists[0]; + Assert.Equal("artist-999", artist.Id); + Assert.Equal("The Rockers", artist.Name); + Assert.Equal(7, artist.AlbumCount); + } + + [Fact] + public void ParseItemsResponse_MixedTypes_SortsCorrectly() + { + // Arrange + var json = @"{ + ""Items"": [ + {""Id"": ""1"", ""Name"": ""Song"", ""Type"": ""Audio""}, + {""Id"": ""2"", ""Name"": ""Album"", ""Type"": ""MusicAlbum""}, + {""Id"": ""3"", ""Name"": ""Artist"", ""Type"": ""MusicArtist""}, + {""Id"": ""4"", ""Name"": ""Another Song"", ""Type"": ""Audio""} + ] + }"; + var doc = JsonDocument.Parse(json); + + // Act + var (songs, albums, artists) = _mapper.ParseItemsResponse(doc); + + // Assert + Assert.Equal(2, songs.Count); + Assert.Single(albums); + Assert.Single(artists); + } + + [Fact] + public void ParseItemsResponse_NullResponse_ReturnsEmptyLists() + { + // Act + var (songs, albums, artists) = _mapper.ParseItemsResponse(null); + + // Assert + Assert.Empty(songs); + Assert.Empty(albums); + Assert.Empty(artists); + } + + [Fact] + public void ParseItemsResponse_EmptyItems_ReturnsEmptyLists() + { + // Arrange + var json = @"{""Items"": [], ""TotalRecordCount"": 0}"; + var doc = JsonDocument.Parse(json); + + // Act + var (songs, albums, artists) = _mapper.ParseItemsResponse(doc); + + // Assert + Assert.Empty(songs); + Assert.Empty(albums); + Assert.Empty(artists); + } + + [Fact] + public void ParseSong_ExtractsArtistFromAlbumArtist_WhenNoArtistsArray() + { + // Arrange + var json = @"{ + ""Id"": ""s1"", + ""Name"": ""Track"", + ""AlbumArtist"": ""Fallback Artist"" + }"; + var element = JsonDocument.Parse(json).RootElement; + + // Act + var song = _mapper.ParseSong(element); + + // Assert + Assert.Equal("Fallback Artist", song.Artist); + } + + [Fact] + public void ParseSong_ExtractsArtistId_FromArtistItems() + { + // Arrange + var json = @"{ + ""Id"": ""s1"", + ""Name"": ""Track"", + ""Artists"": [""Main Artist""], + ""ArtistItems"": [{""Id"": ""art-id-123"", ""Name"": ""Main Artist""}] + }"; + var element = JsonDocument.Parse(json).RootElement; + + // Act + var song = _mapper.ParseSong(element); + + // Assert + Assert.Equal("art-id-123", song.ArtistId); + Assert.Equal("Main Artist", song.Artist); + } + + [Fact] + public void ParseAlbum_ExtractsArtistId_FromAlbumArtists() + { + // Arrange + var json = @"{ + ""Id"": ""alb-1"", + ""Name"": ""The Album"", + ""AlbumArtist"": ""Band Name"", + ""AlbumArtists"": [{""Id"": ""band-id"", ""Name"": ""Band Name""}] + }"; + var element = JsonDocument.Parse(json).RootElement; + + // Act + var album = _mapper.ParseAlbum(element); + + // Assert + Assert.Equal("band-id", album.ArtistId); + } + + [Fact] + public void MergeSearchResults_DeduplicatesArtistsByName() + { + // Arrange + var localArtists = new List + { + new() { Id = "local-1", Name = "The Beatles", IsLocal = true } + }; + + var externalResult = new SearchResult + { + Songs = new List(), + Albums = new List(), + Artists = new List + { + new() { Id = "ext-deezer-artist-1", Name = "The Beatles", IsLocal = false }, + new() { Id = "ext-deezer-artist-2", Name = "Pink Floyd", IsLocal = false } + } + }; + + var playlists = new List(); + + // Act + var (songs, albums, artists) = _mapper.MergeSearchResults( + new List(), new List(), localArtists, externalResult, playlists); + + // Assert - Beatles should not be duplicated, Pink Floyd should be added + Assert.Equal(2, artists.Count); + Assert.Contains(artists, a => a["Id"]!.ToString() == "local-1"); + Assert.Contains(artists, a => a["Id"]!.ToString() == "ext-deezer-artist-2"); + } + + [Fact] + public void MergeSearchResults_IncludesPlaylistsAsAlbums() + { + // Arrange + var playlists = new List + { + new() { Id = "pl-1", Name = "Summer Mix", Provider = "deezer", ExternalId = "123" } + }; + + var externalResult = new SearchResult + { + Songs = new List(), + Albums = new List(), + Artists = new List() + }; + + // Act + var (songs, albums, artists) = _mapper.MergeSearchResults( + new List(), new List(), new List(), externalResult, playlists); + + // Assert + Assert.Single(albums); + Assert.Equal("pl-1", albums[0]["Id"]); + } + + [Fact] + public void ParseAlbumWithTracks_CombinesAlbumAndTracks() + { + // Arrange + var albumJson = @"{ + ""Id"": ""album-1"", + ""Name"": ""Test Album"", + ""Type"": ""MusicAlbum"", + ""AlbumArtist"": ""Test Artist"" + }"; + var tracksJson = @"{ + ""Items"": [ + {""Id"": ""t1"", ""Name"": ""Track 1"", ""Type"": ""Audio""}, + {""Id"": ""t2"", ""Name"": ""Track 2"", ""Type"": ""Audio""} + ] + }"; + + var albumDoc = JsonDocument.Parse(albumJson); + var tracksDoc = JsonDocument.Parse(tracksJson); + + // Act + var album = _mapper.ParseAlbumWithTracks(albumDoc, tracksDoc); + + // Assert + Assert.NotNull(album); + Assert.Equal("album-1", album.Id); + Assert.Equal(2, album.Songs.Count); + } + + [Fact] + public void ParseAlbumWithTracks_NullAlbum_ReturnsNull() + { + // Act + var album = _mapper.ParseAlbumWithTracks(null, null); + + // Assert + Assert.Null(album); + } + + [Fact] + public void ParseArtistWithAlbums_SetsAlbumCount() + { + // Arrange + var artistJson = @"{ + ""Id"": ""art-1"", + ""Name"": ""Test Artist"", + ""Type"": ""MusicArtist"" + }"; + var albumsJson = @"{ + ""Items"": [ + {""Id"": ""a1"", ""Name"": ""Album 1""}, + {""Id"": ""a2"", ""Name"": ""Album 2""}, + {""Id"": ""a3"", ""Name"": ""Album 3""} + ] + }"; + + var artistDoc = JsonDocument.Parse(artistJson); + var albumsDoc = JsonDocument.Parse(albumsJson); + + // Act + var artist = _mapper.ParseArtistWithAlbums(artistDoc, albumsDoc); + + // Assert + Assert.NotNull(artist); + Assert.Equal("art-1", artist.Id); + Assert.Equal(3, artist.AlbumCount); + } + + [Fact] + public void ParseSearchHintsResponse_HandlesSearchHintsFormat() + { + // Arrange + var json = @"{ + ""SearchHints"": [ + {""Id"": ""s1"", ""Name"": ""Song"", ""Type"": ""Audio"", ""Album"": ""Album"", ""AlbumArtist"": ""Artist""}, + {""Id"": ""a1"", ""Name"": ""Album"", ""Type"": ""MusicAlbum"", ""AlbumArtist"": ""Artist""}, + {""Id"": ""ar1"", ""Name"": ""Artist"", ""Type"": ""MusicArtist""} + ], + ""TotalRecordCount"": 3 + }"; + var doc = JsonDocument.Parse(json); + + // Act + var (songs, albums, artists) = _mapper.ParseSearchHintsResponse(doc); + + // Assert + Assert.Single(songs); + Assert.Single(albums); + Assert.Single(artists); + } + + [Fact] + public void ParseSearchHintsResponse_NullResponse_ReturnsEmptyLists() + { + // Act + var (songs, albums, artists) = _mapper.ParseSearchHintsResponse(null); + + // Assert + Assert.Empty(songs); + Assert.Empty(albums); + Assert.Empty(artists); + } +} diff --git a/allstarr.Tests/JellyfinProxyServiceTests.cs b/allstarr.Tests/JellyfinProxyServiceTests.cs new file mode 100644 index 0000000..4c47390 --- /dev/null +++ b/allstarr.Tests/JellyfinProxyServiceTests.cs @@ -0,0 +1,434 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Moq.Protected; +using allstarr.Models.Settings; +using allstarr.Services.Jellyfin; +using System.Net; +using System.Text.Json; + +namespace allstarr.Tests; + +public class JellyfinProxyServiceTests +{ + private readonly JellyfinProxyService _service; + private readonly Mock _mockHandler; + private readonly Mock _mockHttpClientFactory; + private readonly JellyfinSettings _settings; + + public JellyfinProxyServiceTests() + { + _mockHandler = new Mock(); + var httpClient = new HttpClient(_mockHandler.Object); + + _mockHttpClientFactory = new Mock(); + _mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(httpClient); + + _settings = new JellyfinSettings + { + Url = "http://localhost:8096", + ApiKey = "test-api-key-12345", + UserId = "user-guid-here", + ClientName = "TestClient", + DeviceName = "TestDevice", + DeviceId = "test-device-id", + ClientVersion = "1.0.0" + }; + + var httpContext = new DefaultHttpContext(); + var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; + var mockLogger = new Mock>(); + + _service = new JellyfinProxyService( + _mockHttpClientFactory.Object, + Options.Create(_settings), + httpContextAccessor, + mockLogger.Object); + } + + [Fact] + public async Task GetJsonAsync_ValidResponse_ReturnsJsonDocument() + { + // Arrange + var jsonResponse = "{\"Items\":[{\"Id\":\"123\",\"Name\":\"Test Song\"}],\"TotalRecordCount\":1}"; + SetupMockResponse(HttpStatusCode.OK, jsonResponse, "application/json"); + + // Act + var result = await _service.GetJsonAsync("Items"); + + // Assert + Assert.NotNull(result); + Assert.True(result.RootElement.TryGetProperty("Items", out var items)); + Assert.Equal(1, items.GetArrayLength()); + } + + [Fact] + public async Task GetJsonAsync_ServerError_ReturnsNull() + { + // Arrange + SetupMockResponse(HttpStatusCode.InternalServerError, "", "text/plain"); + + // Act + var result = await _service.GetJsonAsync("Items"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task GetJsonAsync_IncludesAuthHeader() + { + // Arrange + HttpRequestMessage? captured = null; + _mockHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => captured = req) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}") + }); + + // Act + await _service.GetJsonAsync("Items"); + + // Assert + Assert.NotNull(captured); + Assert.True(captured!.Headers.Contains("Authorization")); + var authHeader = captured.Headers.GetValues("Authorization").First(); + Assert.Contains("MediaBrowser", authHeader); + Assert.Contains(_settings.ApiKey, authHeader); + Assert.Contains(_settings.ClientName, authHeader); + } + + [Fact] + public async Task GetBytesAsync_ReturnsBodyAndContentType() + { + // Arrange + var imageBytes = new byte[] { 0x89, 0x50, 0x4E, 0x47 }; // PNG magic bytes + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(imageBytes) + }; + response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/png"); + + _mockHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(response); + + // Act + var (body, contentType) = await _service.GetBytesAsync("Items/123/Images/Primary"); + + // Assert + Assert.Equal(imageBytes, body); + Assert.Equal("image/png", contentType); + } + + [Fact] + public async Task GetBytesSafeAsync_OnError_ReturnsSuccessFalse() + { + // Arrange + _mockHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ThrowsAsync(new HttpRequestException("Connection refused")); + + // Act + var (body, contentType, success) = await _service.GetBytesSafeAsync("Items/123/Images/Primary"); + + // Assert + Assert.False(success); + Assert.Null(body); + Assert.Null(contentType); + } + + [Fact] + public async Task SearchAsync_BuildsCorrectQueryParams() + { + // Arrange + HttpRequestMessage? captured = null; + _mockHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => captured = req) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"Items\":[],\"TotalRecordCount\":0}") + }); + + // Act + await _service.SearchAsync("test query", new[] { "Audio", "MusicAlbum" }, 25); + + // Assert + Assert.NotNull(captured); + var url = captured!.RequestUri!.ToString(); + Assert.Contains("searchTerm=test%20query", url); + Assert.Contains("includeItemTypes=Audio%2CMusicAlbum", url); + Assert.Contains("limit=25", url); + Assert.Contains("recursive=true", url); + } + + [Fact] + public async Task GetItemAsync_RequestsCorrectEndpoint() + { + // Arrange + HttpRequestMessage? captured = null; + var itemJson = "{\"Id\":\"abc-123\",\"Name\":\"My Song\",\"Type\":\"Audio\"}"; + _mockHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => captured = req) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(itemJson) + }); + + // Act + var result = await _service.GetItemAsync("abc-123"); + + // Assert + Assert.NotNull(captured); + Assert.Contains("/Items/abc-123", captured!.RequestUri!.ToString()); + Assert.NotNull(result); + } + + [Fact] + public async Task GetArtistsAsync_WithSearchTerm_IncludesInQuery() + { + // Arrange + HttpRequestMessage? captured = null; + _mockHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => captured = req) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"Items\":[],\"TotalRecordCount\":0}") + }); + + // Act + await _service.GetArtistsAsync("Beatles", 10); + + // Assert + Assert.NotNull(captured); + var url = captured!.RequestUri!.ToString(); + Assert.Contains("/Artists", url); + Assert.Contains("searchTerm=Beatles", url); + Assert.Contains("limit=10", url); + } + + [Fact] + public async Task GetImageAsync_WithDimensions_IncludesMaxWidthHeight() + { + // Arrange + HttpRequestMessage? captured = null; + _mockHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => captured = req) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(new byte[] { 1, 2, 3 }) + }); + + // Act + await _service.GetImageAsync("item-123", "Primary", maxWidth: 300, maxHeight: 300); + + // Assert + Assert.NotNull(captured); + var url = captured!.RequestUri!.ToString(); + Assert.Contains("/Items/item-123/Images/Primary", url); + Assert.Contains("maxWidth=300", url); + Assert.Contains("maxHeight=300", url); + } + + [Fact] + public async Task MarkFavoriteAsync_PostsToCorrectEndpoint() + { + // Arrange + HttpRequestMessage? captured = null; + _mockHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => captured = req) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + + // Act + var result = await _service.MarkFavoriteAsync("song-456"); + + // Assert + Assert.True(result); + Assert.NotNull(captured); + Assert.Equal(HttpMethod.Post, captured!.Method); + Assert.Contains($"/Users/{_settings.UserId}/FavoriteItems/song-456", captured.RequestUri!.ToString()); + } + + [Fact] + public async Task MarkFavoriteAsync_WithoutUserId_ReturnsFalse() + { + // Arrange - create service without UserId + var settingsWithoutUser = new JellyfinSettings + { + Url = "http://localhost:8096", + ApiKey = "test-key", + UserId = "" // no user + }; + + var httpContext = new DefaultHttpContext(); + var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; + var mockLogger = new Mock>(); + + var service = new JellyfinProxyService( + _mockHttpClientFactory.Object, + Options.Create(settingsWithoutUser), + httpContextAccessor, + mockLogger.Object); + + // Act + var result = await service.MarkFavoriteAsync("song-456"); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task TestConnectionAsync_ValidServer_ReturnsSuccess() + { + // Arrange + var serverInfo = "{\"ServerName\":\"My Jellyfin\",\"Version\":\"10.8.0\"}"; + SetupMockResponse(HttpStatusCode.OK, serverInfo, "application/json"); + + // Act + var (success, serverName, version) = await _service.TestConnectionAsync(); + + // Assert + Assert.True(success); + Assert.Equal("My Jellyfin", serverName); + Assert.Equal("10.8.0", version); + } + + [Fact] + public async Task TestConnectionAsync_ServerDown_ReturnsFalse() + { + // Arrange + _mockHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ThrowsAsync(new HttpRequestException("Connection refused")); + + // Act + var (success, serverName, version) = await _service.TestConnectionAsync(); + + // Assert + Assert.False(success); + Assert.Null(serverName); + Assert.Null(version); + } + + [Fact] + public async Task GetMusicLibraryIdAsync_WhenConfigured_ReturnsConfiguredId() + { + // Arrange - settings already have LibraryId set + var settingsWithLibrary = new JellyfinSettings + { + Url = "http://localhost:8096", + ApiKey = "test-key", + LibraryId = "configured-library-id" + }; + + var httpContext = new DefaultHttpContext(); + var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; + var mockLogger = new Mock>(); + + var service = new JellyfinProxyService( + _mockHttpClientFactory.Object, + Options.Create(settingsWithLibrary), + httpContextAccessor, + mockLogger.Object); + + // Act + var result = await service.GetMusicLibraryIdAsync(); + + // Assert + Assert.Equal("configured-library-id", result); + } + + [Fact] + public async Task GetMusicLibraryIdAsync_AutoDetects_MusicLibrary() + { + // Arrange + var librariesJson = "{\"Items\":[{\"Id\":\"video-lib\",\"CollectionType\":\"movies\"},{\"Id\":\"music-lib-123\",\"CollectionType\":\"music\"}]}"; + SetupMockResponse(HttpStatusCode.OK, librariesJson, "application/json"); + + var settingsNoLibrary = new JellyfinSettings + { + Url = "http://localhost:8096", + ApiKey = "test-key", + LibraryId = "" // not configured + }; + + var httpContext = new DefaultHttpContext(); + var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; + var mockLogger = new Mock>(); + + var service = new JellyfinProxyService( + _mockHttpClientFactory.Object, + Options.Create(settingsNoLibrary), + httpContextAccessor, + mockLogger.Object); + + // Act + var result = await service.GetMusicLibraryIdAsync(); + + // Assert + Assert.Equal("music-lib-123", result); + } + + [Fact] + public async Task StreamAudioAsync_NullContext_ReturnsError() + { + // Arrange + var httpContextAccessor = new HttpContextAccessor { HttpContext = null }; + var mockLogger = new Mock>(); + + var service = new JellyfinProxyService( + _mockHttpClientFactory.Object, + Options.Create(_settings), + httpContextAccessor, + mockLogger.Object); + + // Act + var result = await service.StreamAudioAsync("song-123", CancellationToken.None); + + // Assert + var objectResult = Assert.IsType(result); + Assert.Equal(500, objectResult.StatusCode); + } + + private void SetupMockResponse(HttpStatusCode statusCode, string content, string contentType) + { + var response = new HttpResponseMessage(statusCode) + { + Content = new StringContent(content) + }; + response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); + + _mockHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(response); + } +} diff --git a/allstarr.Tests/JellyfinResponseBuilderTests.cs b/allstarr.Tests/JellyfinResponseBuilderTests.cs new file mode 100644 index 0000000..0fce213 --- /dev/null +++ b/allstarr.Tests/JellyfinResponseBuilderTests.cs @@ -0,0 +1,292 @@ +using Microsoft.AspNetCore.Mvc; +using allstarr.Models.Domain; +using allstarr.Models.Subsonic; +using allstarr.Services.Jellyfin; + +namespace allstarr.Tests; + +public class JellyfinResponseBuilderTests +{ + private readonly JellyfinResponseBuilder _builder; + + public JellyfinResponseBuilderTests() + { + _builder = new JellyfinResponseBuilder(); + } + + [Fact] + public void ConvertSongToJellyfinItem_SetsCorrectFields() + { + // Arrange + var song = new Song + { + Id = "song-123", + Title = "Test Track", + Artist = "Test Artist", + Album = "Test Album", + AlbumId = "album-456", + ArtistId = "artist-789", + Duration = 245, + Track = 3, + DiscNumber = 1, + Year = 2023, + Genre = "Rock", + IsLocal = true + }; + + // Act + var result = _builder.ConvertSongToJellyfinItem(song); + + // Assert + Assert.Equal("song-123", result["Id"]); + Assert.Equal("Test Track", result["Name"]); + Assert.Equal("Audio", result["Type"]); + Assert.Equal("Test Album", result["Album"]); + Assert.Equal("album-456", result["AlbumId"]); + Assert.Equal(3, result["IndexNumber"]); + Assert.Equal(1, result["ParentIndexNumber"]); + Assert.Equal(2023, result["ProductionYear"]); + Assert.Equal(245 * TimeSpan.TicksPerSecond, result["RunTimeTicks"]); + } + + [Fact] + public void ConvertSongToJellyfinItem_ExternalSong_IncludesProviderIds() + { + // Arrange + var song = new Song + { + Id = "ext-deezer-song-12345", + Title = "External Track", + Artist = "External Artist", + IsLocal = false, + ExternalProvider = "deezer", + ExternalId = "12345", + Isrc = "USRC12345678" + }; + + // Act + var result = _builder.ConvertSongToJellyfinItem(song); + + // Assert + Assert.True(result.ContainsKey("ProviderIds")); + var providerIds = result["ProviderIds"] as Dictionary; + Assert.NotNull(providerIds); + Assert.Equal("12345", providerIds["deezer"]); + Assert.Equal("USRC12345678", providerIds["ISRC"]); + } + + [Fact] + public void ConvertAlbumToJellyfinItem_SetsCorrectFields() + { + // Arrange + var album = new Album + { + Id = "album-456", + Title = "Greatest Hits", + Artist = "Famous Band", + ArtistId = "artist-123", + Year = 2020, + SongCount = 12, + Genre = "Pop", + IsLocal = true + }; + + // Act + var result = _builder.ConvertAlbumToJellyfinItem(album); + + // Assert + Assert.Equal("album-456", result["Id"]); + Assert.Equal("Greatest Hits", result["Name"]); + Assert.Equal("MusicAlbum", result["Type"]); + Assert.Equal(true, result["IsFolder"]); + Assert.Equal("Famous Band", result["AlbumArtist"]); + Assert.Equal(2020, result["ProductionYear"]); + Assert.Equal(12, result["ChildCount"]); + } + + [Fact] + public void ConvertArtistToJellyfinItem_SetsCorrectFields() + { + // Arrange + var artist = new Artist + { + Id = "artist-789", + Name = "The Rockers", + AlbumCount = 5, + IsLocal = true + }; + + // Act + var result = _builder.ConvertArtistToJellyfinItem(artist); + + // Assert + Assert.Equal("artist-789", result["Id"]); + Assert.Equal("The Rockers", result["Name"]); + Assert.Equal("MusicArtist", result["Type"]); + Assert.Equal(true, result["IsFolder"]); + Assert.Equal(5, result["AlbumCount"]); + } + + [Fact] + public void ConvertPlaylistToAlbumItem_SetsPlaylistType() + { + // Arrange + var playlist = new ExternalPlaylist + { + Id = "ext-playlist-deezer-999", + ExternalId = "999", + Name = "Summer Vibes", + Provider = "deezer", + CuratorName = "DJ Cool", + TrackCount = 50, + Duration = 3600, + CreatedDate = new DateTime(2023, 6, 15) + }; + + // Act + var result = _builder.ConvertPlaylistToAlbumItem(playlist); + + // Assert + Assert.Equal("ext-playlist-deezer-999", result["Id"]); + Assert.Equal("Summer Vibes", result["Name"]); + Assert.Equal("Playlist", result["Type"]); + Assert.Equal("DJ Cool", result["AlbumArtist"]); + Assert.Equal(50, result["ChildCount"]); + Assert.Equal(2023, result["ProductionYear"]); + } + + [Fact] + public void ConvertPlaylistToAlbumItem_NoCurator_UsesProvider() + { + // Arrange + var playlist = new ExternalPlaylist + { + Id = "ext-playlist-deezer-888", + ExternalId = "888", + Name = "Top Hits", + Provider = "deezer", + CuratorName = null, + TrackCount = 30 + }; + + // Act + var result = _builder.ConvertPlaylistToAlbumItem(playlist); + + // Assert + Assert.Equal("deezer", result["AlbumArtist"]); + } + + [Fact] + public void CreateItemsResponse_ReturnsPaginatedResult() + { + // Arrange + var songs = new List + { + new() { Id = "1", Title = "Song One", Artist = "Artist", Duration = 200 }, + new() { Id = "2", Title = "Song Two", Artist = "Artist", Duration = 180 } + }; + + // Act + var result = _builder.CreateItemsResponse(songs); + + // Assert + var jsonResult = Assert.IsType(result); + Assert.NotNull(jsonResult.Value); + } + + [Fact] + public void CreateSearchHintsResponse_IncludesAllTypes() + { + // Arrange + var songs = new List { new() { Id = "s1", Title = "Track", Artist = "A" } }; + var albums = new List { new() { Id = "a1", Title = "Album", Artist = "A" } }; + var artists = new List { new() { Id = "ar1", Name = "Artist" } }; + + // Act + var result = _builder.CreateSearchHintsResponse(songs, albums, artists); + + // Assert + var jsonResult = Assert.IsType(result); + Assert.NotNull(jsonResult.Value); + } + + [Fact] + public void CreateError_Returns404ForNotFound() + { + // Act + var result = _builder.CreateError(404, "Item not found"); + + // Assert + var objectResult = Assert.IsType(result); + Assert.Equal(404, objectResult.StatusCode); + } + + [Fact] + public void CreateAlbumResponse_IncludesChildrenForSongs() + { + // Arrange + var album = new Album + { + Id = "album-1", + Title = "Full Album", + Artist = "Artist", + Songs = new List + { + new() { Id = "t1", Title = "Track 1", Artist = "Artist", Track = 1 }, + new() { Id = "t2", Title = "Track 2", Artist = "Artist", Track = 2 } + } + }; + + // Act + var result = _builder.CreateAlbumResponse(album); + + // Assert + var jsonResult = Assert.IsType(result); + Assert.NotNull(jsonResult.Value); + } + + [Fact] + public void CreateArtistResponse_IncludesAlbumsList() + { + // Arrange + var artist = new Artist { Id = "art-1", Name = "Test Artist" }; + var albums = new List + { + new() { Id = "alb-1", Title = "First Album", Artist = "Test Artist" }, + new() { Id = "alb-2", Title = "Second Album", Artist = "Test Artist" } + }; + + // Act + var result = _builder.CreateArtistResponse(artist, albums); + + // Assert + var jsonResult = Assert.IsType(result); + Assert.NotNull(jsonResult.Value); + } + + [Fact] + public void CreatePlaylistAsAlbumResponse_CalculatesTotalDuration() + { + // Arrange + var playlist = new ExternalPlaylist + { + Id = "pl-1", + Name = "My Playlist", + Provider = "deezer", + ExternalId = "123" + }; + var tracks = new List + { + new() { Id = "t1", Title = "Song 1", Duration = 180 }, + new() { Id = "t2", Title = "Song 2", Duration = 240 }, + new() { Id = "t3", Title = "Song 3", Duration = 200 } + }; + + // Act + var result = _builder.CreatePlaylistAsAlbumResponse(playlist, tracks); + + // Assert + var jsonResult = Assert.IsType(result); + Assert.NotNull(jsonResult.Value); + } +} diff --git a/octo-fiesta.Tests/LocalLibraryServiceTests.cs b/allstarr.Tests/LocalLibraryServiceTests.cs similarity index 96% rename from octo-fiesta.Tests/LocalLibraryServiceTests.cs rename to allstarr.Tests/LocalLibraryServiceTests.cs index 61694d5..ac49c6a 100644 --- a/octo-fiesta.Tests/LocalLibraryServiceTests.cs +++ b/allstarr.Tests/LocalLibraryServiceTests.cs @@ -1,9 +1,9 @@ -using octo_fiesta.Services.Local; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; +using allstarr.Services.Local; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -11,7 +11,7 @@ using Moq; using Moq.Protected; using System.Net; -namespace octo_fiesta.Tests; +namespace allstarr.Tests; public class LocalLibraryServiceTests : IDisposable { @@ -21,7 +21,7 @@ public class LocalLibraryServiceTests : IDisposable public LocalLibraryServiceTests() { - _testDownloadPath = Path.Combine(Path.GetTempPath(), "octo-fiesta-tests-" + Guid.NewGuid()); + _testDownloadPath = Path.Combine(Path.GetTempPath(), "allstarr-tests-" + Guid.NewGuid()); Directory.CreateDirectory(_testDownloadPath); var configuration = new ConfigurationBuilder() diff --git a/octo-fiesta.Tests/PlaylistIdHelperTests.cs b/allstarr.Tests/PlaylistIdHelperTests.cs similarity index 99% rename from octo-fiesta.Tests/PlaylistIdHelperTests.cs rename to allstarr.Tests/PlaylistIdHelperTests.cs index cbb3516..53e1c95 100644 --- a/octo-fiesta.Tests/PlaylistIdHelperTests.cs +++ b/allstarr.Tests/PlaylistIdHelperTests.cs @@ -1,7 +1,7 @@ -using octo_fiesta.Services.Common; +using allstarr.Services.Common; using Xunit; -namespace octo_fiesta.Tests; +namespace allstarr.Tests; public class PlaylistIdHelperTests { diff --git a/octo-fiesta.Tests/QobuzDownloadServiceTests.cs b/allstarr.Tests/QobuzDownloadServiceTests.cs similarity index 96% rename from octo-fiesta.Tests/QobuzDownloadServiceTests.cs rename to allstarr.Tests/QobuzDownloadServiceTests.cs index f57438a..14f91c0 100644 --- a/octo-fiesta.Tests/QobuzDownloadServiceTests.cs +++ b/allstarr.Tests/QobuzDownloadServiceTests.cs @@ -1,10 +1,10 @@ -using octo_fiesta.Services; -using octo_fiesta.Services.Qobuz; -using octo_fiesta.Services.Local; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Subsonic; +using allstarr.Services; +using allstarr.Services.Qobuz; +using allstarr.Services.Local; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Subsonic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -12,7 +12,7 @@ using Moq; using Moq.Protected; using System.Net; -namespace octo_fiesta.Tests; +namespace allstarr.Tests; public class QobuzDownloadServiceTests : IDisposable { @@ -28,7 +28,7 @@ public class QobuzDownloadServiceTests : IDisposable public QobuzDownloadServiceTests() { - _testDownloadPath = Path.Combine(Path.GetTempPath(), "octo-fiesta-qobuz-tests-" + Guid.NewGuid()); + _testDownloadPath = Path.Combine(Path.GetTempPath(), "allstarr-qobuz-tests-" + Guid.NewGuid()); Directory.CreateDirectory(_testDownloadPath); _httpMessageHandlerMock = new Mock(); @@ -87,7 +87,7 @@ public class QobuzDownloadServiceTests : IDisposable }); var serviceProviderMock = new Mock(); - serviceProviderMock.Setup(sp => sp.GetService(typeof(octo_fiesta.Services.Subsonic.PlaylistSyncService))) + serviceProviderMock.Setup(sp => sp.GetService(typeof(allstarr.Services.Subsonic.PlaylistSyncService))) .Returns(null); return new QobuzDownloadService( diff --git a/octo-fiesta.Tests/QobuzMetadataServiceTests.cs b/allstarr.Tests/QobuzMetadataServiceTests.cs similarity index 99% rename from octo-fiesta.Tests/QobuzMetadataServiceTests.cs rename to allstarr.Tests/QobuzMetadataServiceTests.cs index 32cf4ab..1086d38 100644 --- a/octo-fiesta.Tests/QobuzMetadataServiceTests.cs +++ b/allstarr.Tests/QobuzMetadataServiceTests.cs @@ -1,14 +1,14 @@ -using octo_fiesta.Services.Qobuz; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Subsonic; +using allstarr.Services.Qobuz; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Subsonic; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using Moq.Protected; using System.Net; -namespace octo_fiesta.Tests; +namespace allstarr.Tests; public class QobuzMetadataServiceTests { diff --git a/octo-fiesta.Tests/SubsonicModelMapperTests.cs b/allstarr.Tests/SubsonicModelMapperTests.cs similarity index 98% rename from octo-fiesta.Tests/SubsonicModelMapperTests.cs rename to allstarr.Tests/SubsonicModelMapperTests.cs index f0d7e41..f3d39ee 100644 --- a/octo-fiesta.Tests/SubsonicModelMapperTests.cs +++ b/allstarr.Tests/SubsonicModelMapperTests.cs @@ -1,14 +1,14 @@ using Microsoft.Extensions.Logging; using Moq; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; -using octo_fiesta.Services.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; +using allstarr.Services.Subsonic; using System.Text; using System.Text.Json; using System.Xml.Linq; -namespace octo_fiesta.Tests; +namespace allstarr.Tests; public class SubsonicModelMapperTests { diff --git a/octo-fiesta.Tests/SubsonicProxyServiceTests.cs b/allstarr.Tests/SubsonicProxyServiceTests.cs similarity index 99% rename from octo-fiesta.Tests/SubsonicProxyServiceTests.cs rename to allstarr.Tests/SubsonicProxyServiceTests.cs index 546608a..fbc9cd8 100644 --- a/octo-fiesta.Tests/SubsonicProxyServiceTests.cs +++ b/allstarr.Tests/SubsonicProxyServiceTests.cs @@ -3,11 +3,11 @@ using Microsoft.Extensions.Options; using Microsoft.AspNetCore.Http; using Moq; using Moq.Protected; -using octo_fiesta.Models.Settings; -using octo_fiesta.Services.Subsonic; +using allstarr.Models.Settings; +using allstarr.Services.Subsonic; using System.Net; -namespace octo_fiesta.Tests; +namespace allstarr.Tests; public class SubsonicProxyServiceTests { diff --git a/octo-fiesta.Tests/SubsonicRequestParserTests.cs b/allstarr.Tests/SubsonicRequestParserTests.cs similarity index 99% rename from octo-fiesta.Tests/SubsonicRequestParserTests.cs rename to allstarr.Tests/SubsonicRequestParserTests.cs index 3e616a6..7fe76a0 100644 --- a/octo-fiesta.Tests/SubsonicRequestParserTests.cs +++ b/allstarr.Tests/SubsonicRequestParserTests.cs @@ -1,8 +1,8 @@ using Microsoft.AspNetCore.Http; -using octo_fiesta.Services.Subsonic; +using allstarr.Services.Subsonic; using System.Text; -namespace octo_fiesta.Tests; +namespace allstarr.Tests; public class SubsonicRequestParserTests { diff --git a/octo-fiesta.Tests/SubsonicResponseBuilderTests.cs b/allstarr.Tests/SubsonicResponseBuilderTests.cs similarity index 99% rename from octo-fiesta.Tests/SubsonicResponseBuilderTests.cs rename to allstarr.Tests/SubsonicResponseBuilderTests.cs index 5581150..86105f3 100644 --- a/octo-fiesta.Tests/SubsonicResponseBuilderTests.cs +++ b/allstarr.Tests/SubsonicResponseBuilderTests.cs @@ -1,10 +1,10 @@ using Microsoft.AspNetCore.Mvc; -using octo_fiesta.Models.Domain; -using octo_fiesta.Services.Subsonic; +using allstarr.Models.Domain; +using allstarr.Services.Subsonic; using System.Text.Json; using System.Xml.Linq; -namespace octo_fiesta.Tests; +namespace allstarr.Tests; public class SubsonicResponseBuilderTests { diff --git a/octo-fiesta.Tests/octo-fiesta.Tests.csproj b/allstarr.Tests/allstarr.Tests.csproj similarity index 86% rename from octo-fiesta.Tests/octo-fiesta.Tests.csproj rename to allstarr.Tests/allstarr.Tests.csproj index dc92d35..bbb3a44 100644 --- a/octo-fiesta.Tests/octo-fiesta.Tests.csproj +++ b/allstarr.Tests/allstarr.Tests.csproj @@ -2,7 +2,7 @@ net9.0 - octo_fiesta.Tests + allstarr.Tests enable enable false @@ -22,7 +22,7 @@ - + diff --git a/octo-fiesta.sln b/allstarr.sln similarity index 88% rename from octo-fiesta.sln rename to allstarr.sln index d16fc2f..b5273f3 100644 --- a/octo-fiesta.sln +++ b/allstarr.sln @@ -1,8 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "octo-fiesta", "octo-fiesta\octo-fiesta.csproj", "{C56EF44B-3AD5-4D4C-A513-726DA8A0E225}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "allstarr", "allstarr\allstarr.csproj", "{C56EF44B-3AD5-4D4C-A513-726DA8A0E225}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "octo-fiesta.Tests", "octo-fiesta.Tests\octo-fiesta.Tests.csproj", "{72E3A16E-7020-4EE0-95D4-FB8FA027ED12}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "allstarr.Tests", "allstarr.Tests\allstarr.Tests.csproj", "{72E3A16E-7020-4EE0-95D4-FB8FA027ED12}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs new file mode 100644 index 0000000..5886dd4 --- /dev/null +++ b/allstarr/Controllers/JellyfinController.cs @@ -0,0 +1,1642 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using System.Text.Json; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Subsonic; +using allstarr.Services; +using allstarr.Services.Common; +using allstarr.Services.Local; +using allstarr.Services.Jellyfin; +using allstarr.Services.Subsonic; + +namespace allstarr.Controllers; + +/// +/// Jellyfin-compatible API controller. Merges local library with external providers +/// (Deezer, Qobuz, SquidWTF). Auth goes through Jellyfin. +/// +[ApiController] +[Route("")] +public class JellyfinController : ControllerBase +{ + private readonly JellyfinSettings _settings; + private readonly IMusicMetadataService _metadataService; + private readonly ILocalLibraryService _localLibraryService; + private readonly IDownloadService _downloadService; + private readonly JellyfinResponseBuilder _responseBuilder; + private readonly JellyfinModelMapper _modelMapper; + private readonly JellyfinProxyService _proxyService; + private readonly PlaylistSyncService? _playlistSyncService; + private readonly ILogger _logger; + + public JellyfinController( + IOptions settings, + IMusicMetadataService metadataService, + ILocalLibraryService localLibraryService, + IDownloadService downloadService, + JellyfinResponseBuilder responseBuilder, + JellyfinModelMapper modelMapper, + JellyfinProxyService proxyService, + ILogger logger, + PlaylistSyncService? playlistSyncService = null) + { + _settings = settings.Value; + _metadataService = metadataService; + _localLibraryService = localLibraryService; + _downloadService = downloadService; + _responseBuilder = responseBuilder; + _modelMapper = modelMapper; + _proxyService = proxyService; + _playlistSyncService = playlistSyncService; + _logger = logger; + + if (string.IsNullOrWhiteSpace(_settings.Url)) + { + throw new InvalidOperationException("JELLYFIN_URL environment variable is not set"); + } + } + + #region Search + + /// + /// Searches local Jellyfin library and external providers. + /// Dedupes artists, combines songs/albums. Works with /Items and /Users/{userId}/Items. + /// + [HttpGet("Items", Order = 1)] + [HttpGet("Users/{userId}/Items", Order = 1)] + public async Task SearchItems( + [FromQuery] string? searchTerm, + [FromQuery] string? includeItemTypes, + [FromQuery] int limit = 20, + [FromQuery] int startIndex = 0, + [FromQuery] string? parentId = null, + [FromQuery] string? artistIds = null, + [FromQuery] string? sortBy = null, + [FromQuery] bool recursive = true, + string? userId = null) + { + _logger.LogInformation("=== SEARCHITEMS V2 CALLED === searchTerm={SearchTerm}, includeItemTypes={ItemTypes}, parentId={ParentId}, artistIds={ArtistIds}, userId={UserId}", + searchTerm, includeItemTypes, parentId, artistIds, userId); + + // If filtering by artist, handle external artists + if (!string.IsNullOrWhiteSpace(artistIds)) + { + var artistId = artistIds.Split(',')[0]; // Take first artist if multiple + var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(artistId); + + if (isExternal) + { + _logger.LogInformation("Fetching albums for external artist: {Provider}/{ExternalId}", provider, externalId); + return await GetExternalChildItems(provider!, externalId!, includeItemTypes); + } + } + + // If no search term, proxy to Jellyfin for browsing + // If Jellyfin returns empty results, we'll just return empty (not mixing browse with external) + if (string.IsNullOrWhiteSpace(searchTerm) && string.IsNullOrWhiteSpace(parentId)) + { + _logger.LogDebug("No search term or parentId, proxying to Jellyfin"); + var browseResult = await _proxyService.GetItemsAsync( + parentId: null, + includeItemTypes: ParseItemTypes(includeItemTypes), + sortBy: sortBy, + limit: limit, + startIndex: startIndex, + clientHeaders: Request.Headers); + + if (browseResult == null) + { + _logger.LogInformation("Jellyfin returned null, returning empty result"); + return new JsonResult(new Dictionary + { + ["Items"] = Array.Empty(), + ["TotalRecordCount"] = 0, + ["StartIndex"] = startIndex + }); + } + + var result = JsonSerializer.Deserialize(browseResult.RootElement.GetRawText()); + if (_logger.IsEnabled(LogLevel.Debug)) + { + var rawText = browseResult.RootElement.GetRawText(); + var preview = rawText.Length > 200 ? rawText[..200] : rawText; + _logger.LogDebug("Jellyfin browse result preview: {Result}", preview); + } + return new JsonResult(result); + } + + // If browsing a specific parent (album, artist, playlist) + if (!string.IsNullOrWhiteSpace(parentId)) + { + // Check if this is the music library root - if so, treat as a search + var isMusicLibrary = parentId == _settings.LibraryId; + + if (!isMusicLibrary || string.IsNullOrWhiteSpace(searchTerm)) + { + _logger.LogDebug("Browsing parent: {ParentId}", parentId); + return await GetChildItems(parentId, includeItemTypes, limit, startIndex, sortBy); + } + + // If searching within music library root, continue to integrated search below + _logger.LogInformation("Searching within music library {ParentId}, including external sources", parentId); + } + + var cleanQuery = searchTerm?.Trim().Trim('"') ?? ""; + _logger.LogInformation("Performing integrated search for: {Query}", cleanQuery); + + // Run local and external searches in parallel + var itemTypes = ParseItemTypes(includeItemTypes); + var jellyfinTask = _proxyService.SearchAsync(cleanQuery, itemTypes, limit, recursive, Request.Headers); + var externalTask = _metadataService.SearchAllAsync(cleanQuery, limit, limit, limit); + + var playlistTask = _settings.EnableExternalPlaylists + ? _metadataService.SearchPlaylistsAsync(cleanQuery, limit) + : Task.FromResult(new List()); + + await Task.WhenAll(jellyfinTask, externalTask, playlistTask); + + var jellyfinResult = await jellyfinTask; + var externalResult = await externalTask; + var playlistResult = await playlistTask; + + _logger.LogInformation("Search results: Jellyfin={JellyfinCount}, External Songs={ExtSongs}, Albums={ExtAlbums}, Artists={ExtArtists}, Playlists={Playlists}", + jellyfinResult != null ? "found" : "null", + externalResult.Songs.Count, + externalResult.Albums.Count, + externalResult.Artists.Count, + playlistResult.Count); + + // Parse Jellyfin results into domain models + var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult); + + // Score and filter Jellyfin results by relevance + var scoredLocalSongs = ScoreSearchResults(cleanQuery, localSongs, s => s.Title, s => s.Artist, isExternal: false); + var scoredLocalAlbums = ScoreSearchResults(cleanQuery, localAlbums, a => a.Title, a => a.Artist, isExternal: false); + var scoredLocalArtists = ScoreSearchResults(cleanQuery, localArtists, a => a.Name, _ => null, isExternal: false); + + // Score external results with a small boost + var scoredExternalSongs = ScoreSearchResults(cleanQuery, externalResult.Songs, s => s.Title, s => s.Artist, isExternal: true); + var scoredExternalAlbums = ScoreSearchResults(cleanQuery, externalResult.Albums, a => a.Title, a => a.Artist, isExternal: true); + var scoredExternalArtists = ScoreSearchResults(cleanQuery, externalResult.Artists, a => a.Name, _ => null, isExternal: true); + + // Merge and sort by score (only include items with score >= 40) + var allSongs = scoredLocalSongs.Concat(scoredExternalSongs) + .Where(x => x.Score >= 40) + .OrderByDescending(x => x.Score) + .Select(x => x.Item) + .ToList(); + + var allAlbums = scoredLocalAlbums.Concat(scoredExternalAlbums) + .Where(x => x.Score >= 40) + .OrderByDescending(x => x.Score) + .Select(x => x.Item) + .ToList(); + + // Dedupe artists by name, keeping highest scored version + var artistScores = scoredLocalArtists.Concat(scoredExternalArtists) + .Where(x => x.Score >= 40) + .GroupBy(x => x.Item.Name, StringComparer.OrdinalIgnoreCase) + .Select(g => g.OrderByDescending(x => x.Score).First()) + .OrderByDescending(x => x.Score) + .Select(x => x.Item) + .ToList(); + + // Convert to Jellyfin format + var mergedSongs = allSongs.Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)).ToList(); + var mergedAlbums = allAlbums.Select(a => _responseBuilder.ConvertAlbumToJellyfinItem(a)).ToList(); + var mergedArtists = artistScores.Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)).ToList(); + + // Add playlists (score them too) + if (playlistResult.Count > 0) + { + var scoredPlaylists = playlistResult + .Select(p => new { Playlist = p, Score = FuzzyMatcher.CalculateSimilarity(cleanQuery, p.Name) }) + .Where(x => x.Score >= 40) + .OrderByDescending(x => x.Score) + .Select(x => _responseBuilder.ConvertPlaylistToJellyfinItem(x.Playlist)) + .ToList(); + + mergedAlbums.AddRange(scoredPlaylists); + } + + _logger.LogInformation("Scored and filtered results: Songs={Songs}, Albums={Albums}, Artists={Artists}", + mergedSongs.Count, mergedAlbums.Count, mergedArtists.Count); + + // Filter by item types if specified + var items = new List>(); + + _logger.LogInformation("Filtering by item types: {ItemTypes}", itemTypes == null ? "null" : string.Join(",", itemTypes)); + + if (itemTypes == null || itemTypes.Length == 0 || itemTypes.Contains("MusicArtist")) + { + _logger.LogInformation("Adding {Count} artists to results", mergedArtists.Count); + items.AddRange(mergedArtists); + } + if (itemTypes == null || itemTypes.Length == 0 || itemTypes.Contains("MusicAlbum") || itemTypes.Contains("Playlist")) + { + _logger.LogInformation("Adding {Count} albums to results", mergedAlbums.Count); + items.AddRange(mergedAlbums); + } + if (itemTypes == null || itemTypes.Length == 0 || itemTypes.Contains("Audio")) + { + _logger.LogInformation("Adding {Count} songs to results", mergedSongs.Count); + items.AddRange(mergedSongs); + } + + // Apply pagination + var pagedItems = items.Skip(startIndex).Take(limit).ToList(); + + _logger.LogInformation("Returning {Count} items (total: {Total})", pagedItems.Count, items.Count); + + try + { + // Return with PascalCase - use ContentResult to bypass JSON serialization issues + var response = new + { + Items = pagedItems, + TotalRecordCount = items.Count, + StartIndex = startIndex + }; + + _logger.LogInformation("About to serialize response..."); + + var json = System.Text.Json.JsonSerializer.Serialize(response, new System.Text.Json.JsonSerializerOptions + { + PropertyNamingPolicy = null, + DictionaryKeyPolicy = null + }); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + var preview = json.Length > 200 ? json[..200] : json; + _logger.LogDebug("JSON response preview: {Json}", preview); + } + + return Content(json, "application/json"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error serializing search response"); + throw; + } + } + + /// + /// Gets child items of a parent (tracks in album, albums for artist). + /// + private async Task GetChildItems( + string parentId, + string? includeItemTypes, + int limit, + int startIndex, + string? sortBy) + { + // Check if this is an external playlist + if (PlaylistIdHelper.IsExternalPlaylist(parentId)) + { + return await GetPlaylistTracks(parentId); + } + + var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(parentId); + + if (isExternal) + { + // Get external album or artist content + return await GetExternalChildItems(provider!, externalId!, includeItemTypes); + } + + // Proxy to Jellyfin for local content + var result = await _proxyService.GetItemsAsync( + parentId: parentId, + includeItemTypes: ParseItemTypes(includeItemTypes), + sortBy: sortBy, + limit: limit, + startIndex: startIndex, + clientHeaders: Request.Headers); + + if (result == null) + { + return _responseBuilder.CreateError(404, "Parent not found"); + } + + return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + } + + /// + /// Quick search endpoint. Works with /Search/Hints and /Users/{userId}/Search/Hints. + /// + [HttpGet("Search/Hints", Order = 1)] + [HttpGet("Users/{userId}/Search/Hints", Order = 1)] + public async Task SearchHints( + [FromQuery] string searchTerm, + [FromQuery] int limit = 20, + [FromQuery] string? includeItemTypes = null, + string? userId = null) + { + if (string.IsNullOrWhiteSpace(searchTerm)) + { + return _responseBuilder.CreateJsonResponse(new + { + SearchHints = Array.Empty(), + TotalRecordCount = 0 + }); + } + + var cleanQuery = searchTerm.Trim().Trim('"'); + var itemTypes = ParseItemTypes(includeItemTypes); + + // Run searches in parallel + var jellyfinTask = _proxyService.SearchAsync(cleanQuery, itemTypes, limit, true, Request.Headers); + var externalTask = _metadataService.SearchAllAsync(cleanQuery, limit, limit, limit); + + await Task.WhenAll(jellyfinTask, externalTask); + + var jellyfinResult = await jellyfinTask; + var externalResult = await externalTask; + + var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult); + + // Merge and convert to search hints format + var allSongs = localSongs.Concat(externalResult.Songs).Take(limit).ToList(); + var allAlbums = localAlbums.Concat(externalResult.Albums).Take(limit).ToList(); + + // Dedupe artists by name + var artistNames = new HashSet(StringComparer.OrdinalIgnoreCase); + var allArtists = new List(); + foreach (var artist in localArtists.Concat(externalResult.Artists)) + { + if (artistNames.Add(artist.Name)) + { + allArtists.Add(artist); + } + } + + return _responseBuilder.CreateSearchHintsResponse( + allSongs.Take(limit).ToList(), + allAlbums.Take(limit).ToList(), + allArtists.Take(limit).ToList()); + } + + #endregion + + #region Items + + /// + /// Gets a single item by ID. + /// + [HttpGet("Items/{itemId}")] + [HttpGet("Users/{userId}/Items/{itemId}")] + public async Task GetItem(string itemId, string? userId = null) + { + if (string.IsNullOrWhiteSpace(itemId)) + { + return _responseBuilder.CreateError(400, "Missing item ID"); + } + + // Check for external playlist + if (PlaylistIdHelper.IsExternalPlaylist(itemId)) + { + return await GetPlaylistAsAlbum(itemId); + } + + var (isExternal, provider, type, externalId) = _localLibraryService.ParseExternalId(itemId); + + if (isExternal) + { + return await GetExternalItem(provider!, type, externalId!); + } + + // Proxy to Jellyfin + var result = await _proxyService.GetItemAsync(itemId, Request.Headers); + if (result == null) + { + return _responseBuilder.CreateError(404, "Item not found"); + } + + return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + } + + /// + /// Gets an external item (song, album, or artist). + /// + private async Task GetExternalItem(string provider, string? type, string externalId) + { + switch (type) + { + case "song": + var song = await _metadataService.GetSongAsync(provider, externalId); + if (song == null) return _responseBuilder.CreateError(404, "Song not found"); + return _responseBuilder.CreateSongResponse(song); + + case "album": + var album = await _metadataService.GetAlbumAsync(provider, externalId); + if (album == null) return _responseBuilder.CreateError(404, "Album not found"); + return _responseBuilder.CreateAlbumResponse(album); + + case "artist": + var artist = await _metadataService.GetArtistAsync(provider, externalId); + if (artist == null) return _responseBuilder.CreateError(404, "Artist not found"); + var albums = await _metadataService.GetArtistAlbumsAsync(provider, externalId); + + // Fill in artist info for albums + foreach (var a in albums) + { + if (string.IsNullOrEmpty(a.Artist)) a.Artist = artist.Name; + if (string.IsNullOrEmpty(a.ArtistId)) a.ArtistId = artist.Id; + } + + return _responseBuilder.CreateArtistResponse(artist, albums); + + default: + // Try song first, then album + var s = await _metadataService.GetSongAsync(provider, externalId); + if (s != null) return _responseBuilder.CreateSongResponse(s); + + var alb = await _metadataService.GetAlbumAsync(provider, externalId); + if (alb != null) return _responseBuilder.CreateAlbumResponse(alb); + + return _responseBuilder.CreateError(404, "Item not found"); + } + } + + /// + /// Gets child items for an external parent (album tracks or artist albums). + /// + private async Task GetExternalChildItems(string provider, string externalId, string? includeItemTypes) + { + var itemTypes = ParseItemTypes(includeItemTypes); + + // Check if asking for audio (album tracks) + if (itemTypes?.Contains("Audio") == true) + { + var album = await _metadataService.GetAlbumAsync(provider, externalId); + if (album == null) + { + return _responseBuilder.CreateError(404, "Album not found"); + } + + return _responseBuilder.CreateItemsResponse(album.Songs); + } + + // Otherwise assume it's artist albums + var albums = await _metadataService.GetArtistAlbumsAsync(provider, externalId); + var artist = await _metadataService.GetArtistAsync(provider, externalId); + + // Fill artist info + if (artist != null) + { + foreach (var a in albums) + { + if (string.IsNullOrEmpty(a.Artist)) a.Artist = artist.Name; + if (string.IsNullOrEmpty(a.ArtistId)) a.ArtistId = artist.Id; + } + } + + return _responseBuilder.CreateAlbumsResponse(albums); + } + + #endregion + + #region Artists + + /// + /// Gets artists from the library. + /// Supports both /Artists and /Artists/AlbumArtists routes. + /// When searchTerm is provided, integrates external search results. + /// + [HttpGet("Artists", Order = 1)] + [HttpGet("Artists/AlbumArtists", Order = 1)] + public async Task GetArtists( + [FromQuery] string? searchTerm, + [FromQuery] int limit = 50, + [FromQuery] int startIndex = 0) + { + _logger.LogInformation("GetArtists called: searchTerm={SearchTerm}, limit={Limit}", searchTerm, limit); + + // If there's a search term, integrate external results + if (!string.IsNullOrWhiteSpace(searchTerm)) + { + var cleanQuery = searchTerm.Trim().Trim('"'); + _logger.LogInformation("Searching artists for: {Query}", cleanQuery); + + // Run local and external searches in parallel + var jellyfinTask = _proxyService.GetArtistsAsync(searchTerm, limit, startIndex, Request.Headers); + var externalTask = _metadataService.SearchArtistsAsync(cleanQuery, limit); + + await Task.WhenAll(jellyfinTask, externalTask); + + var jellyfinResult = await jellyfinTask; + var externalArtists = await externalTask; + + _logger.LogInformation("Artist search results: Jellyfin={JellyfinCount}, External={ExternalCount}", + jellyfinResult != null ? "found" : "null", externalArtists.Count); + + // Parse Jellyfin artists + var localArtists = new List(); + if (jellyfinResult != null && jellyfinResult.RootElement.TryGetProperty("Items", out var items)) + { + foreach (var item in items.EnumerateArray()) + { + localArtists.Add(_modelMapper.ParseArtist(item)); + } + } + + // Merge and deduplicate by name + var artistNames = new HashSet(StringComparer.OrdinalIgnoreCase); + var mergedArtists = new List(); + + foreach (var artist in localArtists) + { + if (artistNames.Add(artist.Name)) + { + mergedArtists.Add(artist); + } + } + + foreach (var artist in externalArtists) + { + if (artistNames.Add(artist.Name)) + { + mergedArtists.Add(artist); + } + } + + _logger.LogInformation("Returning {Count} merged artists", mergedArtists.Count); + + // Convert to Jellyfin format + var artistItems = mergedArtists.Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)).ToList(); + + return _responseBuilder.CreateJsonResponse(new + { + Items = artistItems, + TotalRecordCount = artistItems.Count, + StartIndex = startIndex + }); + } + + // No search term - just proxy to Jellyfin + var result = await _proxyService.GetArtistsAsync(searchTerm, limit, startIndex, Request.Headers); + + if (result == null) + { + return new JsonResult(new Dictionary + { + ["Items"] = Array.Empty(), + ["TotalRecordCount"] = 0, + ["StartIndex"] = startIndex + }); + } + + return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + } + + /// + /// Gets a single artist by ID or name. + /// This route has lower priority to avoid conflicting with Artists/AlbumArtists. + /// + [HttpGet("Artists/{artistIdOrName}", Order = 10)] + public async Task GetArtist(string artistIdOrName) + { + var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(artistIdOrName); + + if (isExternal) + { + var artist = await _metadataService.GetArtistAsync(provider!, externalId!); + if (artist == null) + { + return _responseBuilder.CreateError(404, "Artist not found"); + } + + var albums = await _metadataService.GetArtistAlbumsAsync(provider!, externalId!); + foreach (var a in albums) + { + if (string.IsNullOrEmpty(a.Artist)) a.Artist = artist.Name; + if (string.IsNullOrEmpty(a.ArtistId)) a.ArtistId = artist.Id; + } + + return _responseBuilder.CreateArtistResponse(artist, albums); + } + + // Get local artist from Jellyfin + var jellyfinArtist = await _proxyService.GetArtistAsync(artistIdOrName, Request.Headers); + if (jellyfinArtist == null) + { + return _responseBuilder.CreateError(404, "Artist not found"); + } + + var artistData = _modelMapper.ParseArtist(jellyfinArtist.RootElement); + var artistName = artistData.Name; + var localArtistId = artistData.Id; + + // Get local albums + var localAlbumsResult = await _proxyService.GetItemsAsync( + parentId: null, + includeItemTypes: new[] { "MusicAlbum" }, + sortBy: "SortName", + clientHeaders: Request.Headers); + + var (_, localAlbums, _) = _modelMapper.ParseItemsResponse(localAlbumsResult); + + // Filter to just this artist's albums + var artistAlbums = localAlbums + .Where(a => a.ArtistId == localArtistId || + (a.Artist?.Equals(artistName, StringComparison.OrdinalIgnoreCase) ?? false)) + .ToList(); + + // Search for external albums by this artist + var externalArtists = await _metadataService.SearchArtistsAsync(artistName, 1); + var externalAlbums = new List(); + + if (externalArtists.Count > 0) + { + var extArtist = externalArtists[0]; + if (extArtist.Name.Equals(artistName, StringComparison.OrdinalIgnoreCase)) + { + externalAlbums = await _metadataService.GetArtistAlbumsAsync("deezer", extArtist.ExternalId!); + + // Set artist info to local artist so albums link back correctly + foreach (var a in externalAlbums) + { + if (string.IsNullOrEmpty(a.Artist)) a.Artist = artistName; + if (string.IsNullOrEmpty(a.ArtistId)) a.ArtistId = localArtistId; + } + } + } + + // Deduplicate albums by title + var localAlbumTitles = new HashSet(artistAlbums.Select(a => a.Title), StringComparer.OrdinalIgnoreCase); + var mergedAlbums = artistAlbums.ToList(); + mergedAlbums.AddRange(externalAlbums.Where(a => !localAlbumTitles.Contains(a.Title))); + + return _responseBuilder.CreateArtistResponse(artistData, mergedAlbums); + } + + #endregion + + #region Audio Streaming + + /// + /// Downloads/streams audio. Works with local and external content. + /// + [HttpGet("Items/{itemId}/Download")] + [HttpGet("Items/{itemId}/File")] + public async Task DownloadAudio(string itemId) + { + if (string.IsNullOrWhiteSpace(itemId)) + { + return BadRequest(new { error = "Missing item ID" }); + } + + var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); + + if (!isExternal) + { + // Build path for Jellyfin download/file endpoint + var endpoint = Request.Path.Value?.Contains("/File", StringComparison.OrdinalIgnoreCase) == true ? "File" : "Download"; + var fullPath = $"Items/{itemId}/{endpoint}"; + if (Request.QueryString.HasValue) + { + fullPath = $"{fullPath}{Request.QueryString.Value}"; + } + + return await ProxyJellyfinStream(fullPath, itemId); + } + + // Handle external content + return await StreamExternalContent(provider!, externalId!); + } + + /// + /// Streams audio for a given item. Downloads on-demand for external content. + /// + [HttpGet("Audio/{itemId}/stream")] + [HttpGet("Audio/{itemId}/stream.{container}")] + public async Task StreamAudio(string itemId, string? container = null) + { + if (string.IsNullOrWhiteSpace(itemId)) + { + return BadRequest(new { error = "Missing item ID" }); + } + + var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); + + if (!isExternal) + { + // Build path for Jellyfin stream + var fullPath = string.IsNullOrEmpty(container) + ? $"Audio/{itemId}/stream" + : $"Audio/{itemId}/stream.{container}"; + + if (Request.QueryString.HasValue) + { + fullPath = $"{fullPath}{Request.QueryString.Value}"; + } + + return await ProxyJellyfinStream(fullPath, itemId); + } + + // Handle external content + return await StreamExternalContent(provider!, externalId!); + } + + /// + /// Proxies a stream from Jellyfin with proper header forwarding. + /// + private async Task ProxyJellyfinStream(string path, string itemId) + { + var jellyfinUrl = $"{_settings.Url?.TrimEnd('/')}/{path}"; + + try + { + var request = new HttpRequestMessage(HttpMethod.Get, jellyfinUrl); + + // Forward auth headers + if (Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuth)) + { + request.Headers.TryAddWithoutValidation("X-Emby-Authorization", embyAuth.ToString()); + } + else if (Request.Headers.TryGetValue("Authorization", out var auth)) + { + request.Headers.TryAddWithoutValidation("Authorization", auth.ToString()); + } + + // Forward Range header for seeking + if (Request.Headers.TryGetValue("Range", out var range)) + { + request.Headers.TryAddWithoutValidation("Range", range.ToString()); + } + + var response = await _proxyService.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Jellyfin stream failed: {StatusCode} for {ItemId}", response.StatusCode, itemId); + return StatusCode((int)response.StatusCode); + } + + // Set response status and headers + Response.StatusCode = (int)response.StatusCode; + + var contentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg"; + + if (response.Content.Headers.ContentRange != null) + { + Response.Headers["Content-Range"] = response.Content.Headers.ContentRange.ToString(); + } + + if (response.Headers.AcceptRanges != null) + { + Response.Headers["Accept-Ranges"] = string.Join(", ", response.Headers.AcceptRanges); + } + + if (response.Content.Headers.ContentLength.HasValue) + { + Response.Headers["Content-Length"] = response.Content.Headers.ContentLength.Value.ToString(); + } + + var stream = await response.Content.ReadAsStreamAsync(); + return File(stream, contentType); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to proxy stream from Jellyfin for {ItemId}", itemId); + return StatusCode(500, new { error = $"Streaming failed: {ex.Message}" }); + } + } + + /// + /// Streams external content, using cache if available or downloading on-demand. + /// + private async Task StreamExternalContent(string provider, string externalId) + { + // Check for locally cached file + var localPath = await _localLibraryService.GetLocalPathForExternalSongAsync(provider, externalId); + + if (localPath != null && System.IO.File.Exists(localPath)) + { + var stream = System.IO.File.OpenRead(localPath); + return File(stream, GetContentType(localPath), enableRangeProcessing: true); + } + + // Download and stream on-demand + try + { + var downloadStream = await _downloadService.DownloadAndStreamAsync( + provider, + externalId, + HttpContext.RequestAborted); + + return File(downloadStream, "audio/mpeg", enableRangeProcessing: true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to stream external song {Provider}:{ExternalId}", provider, externalId); + return StatusCode(500, new { error = $"Streaming failed: {ex.Message}" }); + } + } + + /// + /// Universal audio endpoint that redirects to the stream endpoint. + /// + [HttpGet("Audio/{itemId}/universal")] + public Task UniversalAudio(string itemId) + { + return StreamAudio(itemId); + } + + #endregion + + #region Images + + /// + /// Gets the primary image for an item. + /// + [HttpGet("Items/{itemId}/Images/{imageType}")] + [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")] + public async Task GetImage( + string itemId, + string imageType, + int imageIndex = 0, + [FromQuery] int? maxWidth = null, + [FromQuery] int? maxHeight = null) + { + if (string.IsNullOrWhiteSpace(itemId)) + { + return NotFound(); + } + + // Check for external playlist + if (PlaylistIdHelper.IsExternalPlaylist(itemId)) + { + return await GetPlaylistImage(itemId); + } + + var (isExternal, provider, type, externalId) = _localLibraryService.ParseExternalId(itemId); + + if (!isExternal) + { + // Redirect to Jellyfin directly for local content images + var queryString = new List(); + if (maxWidth.HasValue) queryString.Add($"maxWidth={maxWidth.Value}"); + if (maxHeight.HasValue) queryString.Add($"maxHeight={maxHeight.Value}"); + + var path = $"Items/{itemId}/Images/{imageType}"; + if (imageIndex > 0) + { + path = $"Items/{itemId}/Images/{imageType}/{imageIndex}"; + } + + if (queryString.Any()) + { + path = $"{path}?{string.Join("&", queryString)}"; + } + + var jellyfinUrl = $"{_settings.Url?.TrimEnd('/')}/{path}"; + return Redirect(jellyfinUrl); + } + + // Get external cover art URL + string? coverUrl = type switch + { + "artist" => (await _metadataService.GetArtistAsync(provider!, externalId!))?.ImageUrl, + "album" => (await _metadataService.GetAlbumAsync(provider!, externalId!))?.CoverArtUrl, + "song" => (await _metadataService.GetSongAsync(provider!, externalId!))?.CoverArtUrl, + _ => null + }; + + if (string.IsNullOrEmpty(coverUrl)) + { + return NotFound(); + } + + // Fetch and return the image using the proxy service's HttpClient + try + { + var response = await _proxyService.HttpClient.GetAsync(coverUrl); + if (!response.IsSuccessStatusCode) + { + return NotFound(); + } + + var imageBytes = await response.Content.ReadAsByteArrayAsync(); + var contentType = response.Content.Headers.ContentType?.ToString() ?? "image/jpeg"; + return File(imageBytes, contentType); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to fetch cover art from {Url}", coverUrl); + return NotFound(); + } + } + + #endregion + + #region Favorites + + /// + /// Marks an item as favorite. For playlists, triggers a full download. + /// + [HttpPost("Users/{userId}/FavoriteItems/{itemId}")] + public async Task MarkFavorite(string userId, string itemId) + { + // Check if this is an external playlist - trigger download + if (PlaylistIdHelper.IsExternalPlaylist(itemId)) + { + if (_playlistSyncService == null) + { + return _responseBuilder.CreateError(500, "Playlist functionality not enabled"); + } + + _logger.LogInformation("Favoriting external playlist {PlaylistId}, triggering download", itemId); + + // Start download in background + _ = Task.Run(async () => + { + try + { + await _playlistSyncService.DownloadFullPlaylistAsync(itemId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to download playlist {PlaylistId}", itemId); + } + }); + + return Ok(new { IsFavorite = true }); + } + + // Check if this is an external song/album + var (isExternal, _, _) = _localLibraryService.ParseSongId(itemId); + if (isExternal) + { + // External items don't exist in Jellyfin, so we can't favorite them there + // Just return success - the client will show it as favorited + _logger.LogDebug("Favoriting external item {ItemId} (not synced to Jellyfin)", itemId); + return Ok(new { IsFavorite = true }); + } + + // For local Jellyfin items, proxy the request through + var endpoint = $"Users/{userId}/FavoriteItems/{itemId}"; + + try + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{_settings.Url?.TrimEnd('/')}/{endpoint}"); + + // Forward client authentication + if (Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuth)) + { + request.Headers.TryAddWithoutValidation("X-Emby-Authorization", embyAuth.ToString()); + } + else if (Request.Headers.TryGetValue("Authorization", out var auth)) + { + request.Headers.TryAddWithoutValidation("Authorization", auth.ToString()); + } + + var response = await _proxyService.HttpClient.SendAsync(request); + + if (response.IsSuccessStatusCode) + { + return Ok(new { IsFavorite = true }); + } + + _logger.LogWarning("Failed to favorite item in Jellyfin: {StatusCode}", response.StatusCode); + return _responseBuilder.CreateError((int)response.StatusCode, "Failed to mark favorite"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error favoriting item {ItemId}", itemId); + return _responseBuilder.CreateError(500, "Failed to mark favorite"); + } + } + + /// + /// Removes an item from favorites. + /// + [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] + public async Task UnmarkFavorite(string userId, string itemId) + { + // External items can't be unfavorited + var (isExternal, _, _) = _localLibraryService.ParseSongId(itemId); + if (isExternal || PlaylistIdHelper.IsExternalPlaylist(itemId)) + { + return Ok(new { IsFavorite = false }); + } + + // Proxy to Jellyfin to unfavorite + var url = $"Users/{userId}/FavoriteItems/{itemId}"; + + try + { + using var request = new HttpRequestMessage(HttpMethod.Delete, $"{_settings.Url?.TrimEnd('/')}/{url}"); + + // Forward client authentication + if (Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuth)) + { + request.Headers.TryAddWithoutValidation("X-Emby-Authorization", embyAuth.ToString()); + } + else if (Request.Headers.TryGetValue("Authorization", out var auth)) + { + request.Headers.TryAddWithoutValidation("Authorization", auth.ToString()); + } + + var response = await _proxyService.HttpClient.SendAsync(request); + + if (response.IsSuccessStatusCode) + { + return Ok(new { IsFavorite = false }); + } + + return _responseBuilder.CreateError(500, "Failed to unfavorite item"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error unfavoriting item {ItemId}", itemId); + return _responseBuilder.CreateError(500, "Failed to unfavorite item"); + } + } + + #endregion + + #region Playlists + + /// + /// Gets playlist tracks displayed as an album. + /// + private async Task GetPlaylistAsAlbum(string playlistId) + { + try + { + var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); + + var playlist = await _metadataService.GetPlaylistAsync(provider, externalId); + if (playlist == null) + { + return _responseBuilder.CreateError(404, "Playlist not found"); + } + + var tracks = await _metadataService.GetPlaylistTracksAsync(provider, externalId); + + // Cache tracks for playlist sync + if (_playlistSyncService != null) + { + foreach (var track in tracks) + { + if (!string.IsNullOrEmpty(track.ExternalId)) + { + var trackId = $"ext-{provider}-{track.ExternalId}"; + _playlistSyncService.AddTrackToPlaylistCache(trackId, playlistId); + } + } + _logger.LogDebug("Cached {Count} tracks for playlist {PlaylistId}", tracks.Count, playlistId); + } + + return _responseBuilder.CreatePlaylistAsAlbumResponse(playlist, tracks); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting playlist {PlaylistId}", playlistId); + return _responseBuilder.CreateError(500, "Failed to get playlist"); + } + } + + /// + /// Gets playlist tracks as child items. + /// + private async Task GetPlaylistTracks(string playlistId) + { + try + { + var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); + var tracks = await _metadataService.GetPlaylistTracksAsync(provider, externalId); + + return _responseBuilder.CreateItemsResponse(tracks); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting playlist tracks {PlaylistId}", playlistId); + return _responseBuilder.CreateError(500, "Failed to get playlist tracks"); + } + } + + /// + /// Gets a playlist cover image. + /// + private async Task GetPlaylistImage(string playlistId) + { + try + { + var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); + var playlist = await _metadataService.GetPlaylistAsync(provider, externalId); + + if (playlist == null || string.IsNullOrEmpty(playlist.CoverUrl)) + { + return NotFound(); + } + + var response = await _proxyService.HttpClient.GetAsync(playlist.CoverUrl); + if (!response.IsSuccessStatusCode) + { + return NotFound(); + } + + var imageBytes = await response.Content.ReadAsByteArrayAsync(); + var contentType = response.Content.Headers.ContentType?.ToString() ?? "image/jpeg"; + return File(imageBytes, contentType); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to get playlist image {PlaylistId}", playlistId); + return NotFound(); + } + } + + #endregion + + #region Authentication + + /// + /// Authenticates a user by username and password. + /// This is the primary login endpoint for Jellyfin clients. + /// + [HttpPost("Users/AuthenticateByName")] + public async Task AuthenticateByName() + { + try + { + // Enable buffering to allow multiple reads of the request body + Request.EnableBuffering(); + + // Read the request body + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + + // Reset stream position + Request.Body.Position = 0; + + _logger.LogInformation("Authentication request received"); + // DO NOT log request body or detailed headers - contains password + + // Forward to Jellyfin server with client headers + var result = await _proxyService.PostJsonAsync("Users/AuthenticateByName", body, Request.Headers); + + if (result == null) + { + _logger.LogWarning("Authentication failed - no response from Jellyfin"); + return Unauthorized(new { error = "Authentication failed" }); + } + + _logger.LogInformation("Authentication successful"); + return Content(result.RootElement.GetRawText(), "application/json"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during authentication"); + return StatusCode(500, new { error = $"Authentication error: {ex.Message}" }); + } + } + + #endregion + + #region Recommendations & Instant Mix + + /// + /// Gets similar items for a given item. + /// For external items, searches for similar content from the provider. + /// + [HttpGet("Items/{itemId}/Similar")] + [HttpGet("Songs/{itemId}/Similar")] + public async Task GetSimilarItems( + string itemId, + [FromQuery] int limit = 50, + [FromQuery] string? fields = null, + [FromQuery] string? userId = null) + { + var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); + + if (isExternal) + { + try + { + // Get the original song to find similar content + var song = await _metadataService.GetSongAsync(provider!, externalId!); + if (song == null) + { + return _responseBuilder.CreateJsonResponse(new + { + Items = Array.Empty(), + TotalRecordCount = 0 + }); + } + + // Search for similar songs using artist and genre + var searchQuery = $"{song.Artist}"; + var searchResult = await _metadataService.SearchSongsAsync(searchQuery, limit); + + // Filter out the original song and convert to Jellyfin format + var similarSongs = searchResult + .Where(s => s.Id != itemId) + .Take(limit) + .Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)) + .ToList(); + + return _responseBuilder.CreateJsonResponse(new + { + Items = similarSongs, + TotalRecordCount = similarSongs.Count + }); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to get similar items for external song {ItemId}", itemId); + return _responseBuilder.CreateJsonResponse(new + { + Items = Array.Empty(), + TotalRecordCount = 0 + }); + } + } + + // For local items, proxy to Jellyfin + var queryParams = new Dictionary + { + ["limit"] = limit.ToString() + }; + + if (!string.IsNullOrEmpty(fields)) + { + queryParams["fields"] = fields; + } + + if (!string.IsNullOrEmpty(userId)) + { + queryParams["userId"] = userId; + } + + var result = await _proxyService.GetJsonAsync($"Items/{itemId}/Similar", queryParams, Request.Headers); + + if (result == null) + { + return _responseBuilder.CreateJsonResponse(new + { + Items = Array.Empty(), + TotalRecordCount = 0 + }); + } + + return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + } + + /// + /// Gets an instant mix for a given item. + /// For external items, creates a mix from the artist's other songs. + /// + [HttpGet("Songs/{itemId}/InstantMix")] + [HttpGet("Items/{itemId}/InstantMix")] + public async Task GetInstantMix( + string itemId, + [FromQuery] int limit = 50, + [FromQuery] string? fields = null, + [FromQuery] string? userId = null) + { + var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); + + if (isExternal) + { + try + { + // Get the original song + var song = await _metadataService.GetSongAsync(provider!, externalId!); + if (song == null) + { + return _responseBuilder.CreateJsonResponse(new + { + Items = Array.Empty(), + TotalRecordCount = 0 + }); + } + + // Get artist's albums to build a mix + var mixSongs = new List(); + + // Try to get artist albums + if (!string.IsNullOrEmpty(song.ExternalProvider) && !string.IsNullOrEmpty(song.ArtistId)) + { + var artistExternalId = song.ArtistId.Replace($"ext-{song.ExternalProvider}-artist-", ""); + var albums = await _metadataService.GetArtistAlbumsAsync(song.ExternalProvider, artistExternalId); + + // Get songs from a few albums + foreach (var album in albums.Take(3)) + { + var fullAlbum = await _metadataService.GetAlbumAsync(song.ExternalProvider, album.ExternalId!); + if (fullAlbum != null) + { + mixSongs.AddRange(fullAlbum.Songs); + } + + if (mixSongs.Count >= limit) break; + } + } + + // If we don't have enough songs, search for more by the artist + if (mixSongs.Count < limit) + { + var searchResult = await _metadataService.SearchSongsAsync(song.Artist, limit); + mixSongs.AddRange(searchResult.Where(s => !mixSongs.Any(m => m.Id == s.Id))); + } + + // Shuffle and limit + var random = new Random(); + var shuffledMix = mixSongs + .Where(s => s.Id != itemId) // Exclude the seed song + .OrderBy(_ => random.Next()) + .Take(limit) + .Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)) + .ToList(); + + return _responseBuilder.CreateJsonResponse(new + { + Items = shuffledMix, + TotalRecordCount = shuffledMix.Count + }); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to create instant mix for external song {ItemId}", itemId); + return _responseBuilder.CreateJsonResponse(new + { + Items = Array.Empty(), + TotalRecordCount = 0 + }); + } + } + + // For local items, proxy to Jellyfin + var queryParams = new Dictionary + { + ["limit"] = limit.ToString() + }; + + if (!string.IsNullOrEmpty(fields)) + { + queryParams["fields"] = fields; + } + + if (!string.IsNullOrEmpty(userId)) + { + queryParams["userId"] = userId; + } + + var result = await _proxyService.GetJsonAsync($"Songs/{itemId}/InstantMix", queryParams, Request.Headers); + + if (result == null) + { + return _responseBuilder.CreateJsonResponse(new + { + Items = Array.Empty(), + TotalRecordCount = 0 + }); + } + + return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + } + + #endregion + + #region System & Proxy + + /// + /// Returns public server info. + /// + [HttpGet("System/Info/Public")] + public async Task GetPublicSystemInfo() + { + var (success, serverName, version) = await _proxyService.TestConnectionAsync(); + + return _responseBuilder.CreateJsonResponse(new + { + LocalAddress = Request.Host.ToString(), + ServerName = serverName ?? "Allstarr", + Version = version ?? "1.0.0", + ProductName = "Allstarr (Jellyfin Proxy)", + OperatingSystem = Environment.OSVersion.Platform.ToString(), + Id = _settings.DeviceId, + StartupWizardCompleted = true + }); + } + + /// + /// Root path handler - redirects to Jellyfin web UI. + /// + [HttpGet("", Order = 99)] + public async Task ProxyRootRequest() + { + return await ProxyRequest("web/index.html"); + } + + /// + /// Catch-all endpoint that proxies unhandled requests to Jellyfin transparently. + /// This route has the lowest priority and should only match requests that don't have SearchTerm. + /// + [HttpGet("{**path}", Order = 100)] + [HttpPost("{**path}", Order = 100)] + public async Task ProxyRequest(string path) + { + // Handle non-JSON responses (robots.txt, etc.) + if (path.EndsWith(".txt", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) + { + var fullPath = path; + if (Request.QueryString.HasValue) + { + fullPath = $"{path}{Request.QueryString.Value}"; + } + + var url = $"{_settings.Url?.TrimEnd('/')}/{fullPath}"; + + try + { + var response = await _proxyService.HttpClient.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + var contentType = response.Content.Headers.ContentType?.ToString() ?? "text/plain"; + return Content(content, contentType); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to proxy non-JSON request for {Path}", path); + return NotFound(); + } + } + + // Check if this is a search request that should be handled by specific endpoints + var searchTerm = Request.Query["SearchTerm"].ToString(); + + if (!string.IsNullOrWhiteSpace(searchTerm)) + { + _logger.LogInformation("ProxyRequest intercepting search request: Path={Path}, SearchTerm={SearchTerm}", path, searchTerm); + + // Item search: /users/{userId}/items or /items + if (path.EndsWith("/items", StringComparison.OrdinalIgnoreCase) || path.Equals("items", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Redirecting to SearchItems"); + return await SearchItems( + searchTerm: searchTerm, + includeItemTypes: Request.Query["IncludeItemTypes"], + limit: int.TryParse(Request.Query["Limit"], out var limit) ? limit : 100, + startIndex: int.TryParse(Request.Query["StartIndex"], out var start) ? start : 0, + parentId: Request.Query["ParentId"], + sortBy: Request.Query["SortBy"], + recursive: Request.Query["Recursive"].ToString().Equals("true", StringComparison.OrdinalIgnoreCase), + userId: path.Contains("/users/", StringComparison.OrdinalIgnoreCase) && path.Split('/').Length > 2 ? path.Split('/')[2] : null); + } + + // Artist search: /artists/albumartists or /artists + if (path.Contains("/artists", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Redirecting to GetArtists"); + return await GetArtists( + searchTerm: searchTerm, + limit: int.TryParse(Request.Query["Limit"], out var limit) ? limit : 50, + startIndex: int.TryParse(Request.Query["StartIndex"], out var start) ? start : 0); + } + } + + try + { + // Include query string in the path + var fullPath = path; + if (Request.QueryString.HasValue) + { + fullPath = $"{path}{Request.QueryString.Value}"; + } + + JsonDocument? result; + + if (HttpContext.Request.Method == HttpMethod.Post.Method) + { + // Enable buffering BEFORE any reads + Request.EnableBuffering(); + + // Log request details for debugging + _logger.LogInformation("POST request to {Path}: Method={Method}, ContentType={ContentType}, ContentLength={ContentLength}", + fullPath, Request.Method, Request.ContentType, Request.ContentLength); + + // Read body using StreamReader with proper encoding + string body; + using (var reader = new StreamReader(Request.Body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + body = await reader.ReadToEndAsync(); + } + + // Reset stream position after reading + Request.Body.Position = 0; + + if (string.IsNullOrWhiteSpace(body)) + { + _logger.LogWarning("Empty POST body for {Path}, ContentLength={ContentLength}, ContentType={ContentType}", + fullPath, Request.ContentLength, Request.ContentType); + } + else + { + _logger.LogInformation("POST body for {Path}: {BodyLength} bytes, ContentType={ContentType}", + fullPath, body.Length, Request.ContentType); + + // Always log body content for playback endpoints to debug the issue + if (fullPath.Contains("Playing", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("POST body content: {Body}", body); + } + } + + result = await _proxyService.PostJsonAsync(fullPath, body, Request.Headers); + } + else + { + // Forward GET requests transparently with authentication headers and query string + result = await _proxyService.GetJsonAsync(fullPath, null, Request.Headers); + } + + if (result == null) + { + // Return 204 No Content for successful requests with no body + // (e.g., /sessions/playing, /sessions/playing/progress) + return NoContent(); + } + + return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Proxy request failed for {Path}", path); + return _responseBuilder.CreateError(502, $"Proxy error: {ex.Message}"); + } + } + + #endregion + + #region Helpers + + private static string[]? ParseItemTypes(string? includeItemTypes) + { + if (string.IsNullOrWhiteSpace(includeItemTypes)) + { + return null; + } + + return includeItemTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + private static string GetContentType(string filePath) + { + var extension = Path.GetExtension(filePath).ToLowerInvariant(); + return extension switch + { + ".mp3" => "audio/mpeg", + ".flac" => "audio/flac", + ".ogg" => "audio/ogg", + ".m4a" => "audio/mp4", + ".wav" => "audio/wav", + ".aac" => "audio/aac", + _ => "audio/mpeg" + }; + } + + /// + /// Scores search results based on fuzzy matching against the query. + /// Returns items with their relevance scores. + /// External results get a small boost to prioritize the larger catalog. + /// + private static List<(T Item, int Score)> ScoreSearchResults( + string query, + List items, + Func primaryField, + Func secondaryField, + bool isExternal = false) + { + return items.Select(item => + { + var primary = primaryField(item) ?? ""; + var secondary = secondaryField(item) ?? ""; + + // Score against primary field (title/name) + var primaryScore = FuzzyMatcher.CalculateSimilarity(query, primary); + + // Score against secondary field (artist) if provided + var secondaryScore = string.IsNullOrEmpty(secondary) + ? 0 + : FuzzyMatcher.CalculateSimilarity(query, secondary); + + // Use the better of the two scores + var baseScore = Math.Max(primaryScore, secondaryScore); + + // Give external results a small boost (+5 points) to prioritize the larger catalog + // This means external results will rank slightly higher when scores are close + var finalScore = isExternal ? Math.Min(100, baseScore + 5) : baseScore; + + return (item, finalScore); + }).ToList(); + } + + #endregion +} +// force rebuild Sun Jan 25 13:22:47 EST 2026 diff --git a/octo-fiesta/Controllers/SubSonicController.cs b/allstarr/Controllers/SubSonicController.cs similarity index 98% rename from octo-fiesta/Controllers/SubSonicController.cs rename to allstarr/Controllers/SubSonicController.cs index e2e49f6..97183d4 100644 --- a/octo-fiesta/Controllers/SubSonicController.cs +++ b/allstarr/Controllers/SubSonicController.cs @@ -3,17 +3,17 @@ using System.Xml.Linq; using System.Text; using System.Text.Json; using Microsoft.Extensions.Options; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; -using octo_fiesta.Services; -using octo_fiesta.Services.Common; -using octo_fiesta.Services.Local; -using octo_fiesta.Services.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; +using allstarr.Services; +using allstarr.Services.Common; +using allstarr.Services.Local; +using allstarr.Services.Subsonic; -namespace octo_fiesta.Controllers; +namespace allstarr.Controllers; [ApiController] [Route("")] diff --git a/allstarr/Filters/JellyfinAuthFilter.cs b/allstarr/Filters/JellyfinAuthFilter.cs new file mode 100644 index 0000000..be8702b --- /dev/null +++ b/allstarr/Filters/JellyfinAuthFilter.cs @@ -0,0 +1,240 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Options; +using allstarr.Models.Settings; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace allstarr.Filters; + +/// +/// Authentication filter for Jellyfin API endpoints. +/// Validates client credentials against configured username and API key. +/// Clients can authenticate via: +/// - Authorization header: MediaBrowser Token="apikey" +/// - X-Emby-Token header +/// - Query parameter: api_key +/// - JSON body (for login endpoints): Username/Pw fields +/// +public partial class JellyfinAuthFilter : IAsyncActionFilter +{ + private readonly JellyfinSettings _settings; + private readonly ILogger _logger; + + public JellyfinAuthFilter( + IOptions settings, + ILogger logger) + { + _settings = settings.Value; + _logger = logger; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // Skip auth if no credentials configured (open mode) + if (string.IsNullOrEmpty(_settings.ClientUsername) || string.IsNullOrEmpty(_settings.ApiKey)) + { + _logger.LogDebug("Auth skipped - no client credentials configured"); + await next(); + return; + } + + var request = context.HttpContext.Request; + + // Try to extract credentials from various sources + var (username, token) = await ExtractCredentialsAsync(request); + + // Validate credentials + if (!ValidateCredentials(username, token)) + { + _logger.LogWarning("Authentication failed for user '{Username}' from {IP}", + username ?? "unknown", + context.HttpContext.Connection.RemoteIpAddress); + + context.Result = new UnauthorizedObjectResult(new + { + error = "Invalid credentials", + message = "Authentication required. Provide valid username and API key." + }); + return; + } + + _logger.LogDebug("Authentication successful for user '{Username}'", username); + await next(); + } + + private async Task<(string? username, string? token)> ExtractCredentialsAsync(HttpRequest request) + { + string? username = null; + string? token = null; + + // 1. Check Authorization header (MediaBrowser format) + if (request.Headers.TryGetValue("Authorization", out var authHeader)) + { + var authValue = authHeader.ToString(); + + // Parse MediaBrowser auth header: MediaBrowser Client="...", Token="..." + if (authValue.StartsWith("MediaBrowser", StringComparison.OrdinalIgnoreCase)) + { + token = ExtractTokenFromMediaBrowser(authValue); + username = ExtractUserIdFromMediaBrowser(authValue); + } + // Basic auth: Basic base64(username:password) + else if (authValue.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) + { + (username, token) = ParseBasicAuth(authValue); + } + } + + // 2. Check X-Emby-Token header + if (string.IsNullOrEmpty(token) && request.Headers.TryGetValue("X-Emby-Token", out var embyToken)) + { + token = embyToken.ToString(); + } + + // 3. Check X-MediaBrowser-Token header + if (string.IsNullOrEmpty(token) && request.Headers.TryGetValue("X-MediaBrowser-Token", out var mbToken)) + { + token = mbToken.ToString(); + } + + // 4. Check X-Emby-Authorization header (alternative format) + if (string.IsNullOrEmpty(token) && request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuth)) + { + token = ExtractTokenFromMediaBrowser(embyAuth.ToString()); + if (string.IsNullOrEmpty(username)) + { + username = ExtractUserIdFromMediaBrowser(embyAuth.ToString()); + } + } + + // 5. Check query parameters + if (string.IsNullOrEmpty(token)) + { + token = request.Query["api_key"].FirstOrDefault() + ?? request.Query["ApiKey"].FirstOrDefault() + ?? request.Query["X-Emby-Token"].FirstOrDefault(); + } + + if (string.IsNullOrEmpty(username)) + { + username = request.Query["userId"].FirstOrDefault() + ?? request.Query["UserId"].FirstOrDefault() + ?? request.Query["u"].FirstOrDefault(); + } + + // 6. Check JSON body for login endpoints (Jellyfin: Username/Pw, Navidrome: username/password) + if ((string.IsNullOrEmpty(username) || string.IsNullOrEmpty(token)) && + request.ContentType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true && + request.ContentLength > 0) + { + var (bodyUsername, bodyPassword) = await ExtractCredentialsFromBodyAsync(request); + if (string.IsNullOrEmpty(username)) username = bodyUsername; + if (string.IsNullOrEmpty(token)) token = bodyPassword; + } + + return (username, token); + } + + private async Task<(string? username, string? password)> ExtractCredentialsFromBodyAsync(HttpRequest request) + { + try + { + request.EnableBuffering(); + request.Body.Position = 0; + + using var reader = new StreamReader(request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + request.Body.Position = 0; + + if (string.IsNullOrEmpty(body)) return (null, null); + + using var doc = JsonDocument.Parse(body); + var root = doc.RootElement; + + // Try Jellyfin format: Username, Pw + string? username = null; + string? password = null; + + if (root.TryGetProperty("Username", out var usernameProp)) + username = usernameProp.GetString(); + else if (root.TryGetProperty("username", out var usernameLowerProp)) + username = usernameLowerProp.GetString(); + + if (root.TryGetProperty("Pw", out var pwProp)) + password = pwProp.GetString(); + else if (root.TryGetProperty("pw", out var pwLowerProp)) + password = pwLowerProp.GetString(); + else if (root.TryGetProperty("Password", out var passwordProp)) + password = passwordProp.GetString(); + else if (root.TryGetProperty("password", out var passwordLowerProp)) + password = passwordLowerProp.GetString(); + + return (username, password); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to parse credentials from request body"); + return (null, null); + } + } + + private string? ExtractTokenFromMediaBrowser(string header) + { + var match = TokenRegex().Match(header); + return match.Success ? match.Groups[1].Value : null; + } + + private string? ExtractUserIdFromMediaBrowser(string header) + { + var match = UserIdRegex().Match(header); + return match.Success ? match.Groups[1].Value : null; + } + + private static (string? username, string? password) ParseBasicAuth(string authHeader) + { + try + { + var base64 = authHeader["Basic ".Length..].Trim(); + var bytes = Convert.FromBase64String(base64); + var credentials = System.Text.Encoding.UTF8.GetString(bytes); + var parts = credentials.Split(':', 2); + + return parts.Length == 2 ? (parts[0], parts[1]) : (null, null); + } + catch + { + return (null, null); + } + } + + private bool ValidateCredentials(string? username, string? token) + { + // Must have token (API key used as password) + if (string.IsNullOrEmpty(token)) + { + return false; + } + + // Token must match API key + if (!string.Equals(token, _settings.ApiKey, StringComparison.Ordinal)) + { + return false; + } + + // If username provided, it must match configured client username + if (!string.IsNullOrEmpty(username) && + !string.Equals(username, _settings.ClientUsername, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + + [GeneratedRegex(@"Token=""([^""]+)""", RegexOptions.IgnoreCase)] + private static partial Regex TokenRegex(); + + [GeneratedRegex(@"UserId=""([^""]+)""", RegexOptions.IgnoreCase)] + private static partial Regex UserIdRegex(); +} diff --git a/octo-fiesta/Middleware/GlobalExceptionHandler.cs b/allstarr/Middleware/GlobalExceptionHandler.cs similarity index 98% rename from octo-fiesta/Middleware/GlobalExceptionHandler.cs rename to allstarr/Middleware/GlobalExceptionHandler.cs index 3b01cce..84e1126 100644 --- a/octo-fiesta/Middleware/GlobalExceptionHandler.cs +++ b/allstarr/Middleware/GlobalExceptionHandler.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Diagnostics; -namespace octo_fiesta.Middleware; +namespace allstarr.Middleware; /// /// Global exception handler that catches unhandled exceptions and returns appropriate Subsonic API error responses diff --git a/octo-fiesta/Models/Domain/Album.cs b/allstarr/Models/Domain/Album.cs similarity index 94% rename from octo-fiesta/Models/Domain/Album.cs rename to allstarr/Models/Domain/Album.cs index bd272bb..c5cc95c 100644 --- a/octo-fiesta/Models/Domain/Album.cs +++ b/allstarr/Models/Domain/Album.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Models.Domain; +namespace allstarr.Models.Domain; /// /// Represents an album diff --git a/octo-fiesta/Models/Domain/Artist.cs b/allstarr/Models/Domain/Artist.cs similarity index 91% rename from octo-fiesta/Models/Domain/Artist.cs rename to allstarr/Models/Domain/Artist.cs index 276a88b..5340a79 100644 --- a/octo-fiesta/Models/Domain/Artist.cs +++ b/allstarr/Models/Domain/Artist.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Models.Domain; +namespace allstarr.Models.Domain; /// /// Represents an artist diff --git a/octo-fiesta/Models/Domain/Song.cs b/allstarr/Models/Domain/Song.cs similarity index 98% rename from octo-fiesta/Models/Domain/Song.cs rename to allstarr/Models/Domain/Song.cs index 01c9796..87e5925 100644 --- a/octo-fiesta/Models/Domain/Song.cs +++ b/allstarr/Models/Domain/Song.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Models.Domain; +namespace allstarr.Models.Domain; /// /// Represents a song (local or external) diff --git a/octo-fiesta/Models/Download/DownloadInfo.cs b/allstarr/Models/Download/DownloadInfo.cs similarity index 93% rename from octo-fiesta/Models/Download/DownloadInfo.cs rename to allstarr/Models/Download/DownloadInfo.cs index 608295d..b73d661 100644 --- a/octo-fiesta/Models/Download/DownloadInfo.cs +++ b/allstarr/Models/Download/DownloadInfo.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Models.Download; +namespace allstarr.Models.Download; /// /// Information about an ongoing or completed download diff --git a/octo-fiesta/Models/Download/DownloadStatus.cs b/allstarr/Models/Download/DownloadStatus.cs similarity index 79% rename from octo-fiesta/Models/Download/DownloadStatus.cs rename to allstarr/Models/Download/DownloadStatus.cs index 5d5d4f9..0e82e46 100644 --- a/octo-fiesta/Models/Download/DownloadStatus.cs +++ b/allstarr/Models/Download/DownloadStatus.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Models.Download; +namespace allstarr.Models.Download; /// /// Download status of a song diff --git a/octo-fiesta/Models/Search/SearchResult.cs b/allstarr/Models/Search/SearchResult.cs similarity index 79% rename from octo-fiesta/Models/Search/SearchResult.cs rename to allstarr/Models/Search/SearchResult.cs index 25280a4..633ddb0 100644 --- a/octo-fiesta/Models/Search/SearchResult.cs +++ b/allstarr/Models/Search/SearchResult.cs @@ -1,6 +1,6 @@ -namespace octo_fiesta.Models.Search; +namespace allstarr.Models.Search; -using octo_fiesta.Models.Domain; +using allstarr.Models.Domain; /// /// Search result combining local and external results diff --git a/octo-fiesta/Models/Settings/DeezerSettings.cs b/allstarr/Models/Settings/DeezerSettings.cs similarity index 94% rename from octo-fiesta/Models/Settings/DeezerSettings.cs rename to allstarr/Models/Settings/DeezerSettings.cs index ecc50b9..1fe35d3 100644 --- a/octo-fiesta/Models/Settings/DeezerSettings.cs +++ b/allstarr/Models/Settings/DeezerSettings.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Models.Settings; +namespace allstarr.Models.Settings; /// /// Configuration for the Deezer downloader and metadata service diff --git a/allstarr/Models/Settings/JellyfinSettings.cs b/allstarr/Models/Settings/JellyfinSettings.cs new file mode 100644 index 0000000..22d212b --- /dev/null +++ b/allstarr/Models/Settings/JellyfinSettings.cs @@ -0,0 +1,67 @@ +namespace allstarr.Models.Settings; + +/// +/// Configuration for Jellyfin media server backend +/// +public class JellyfinSettings +{ + /// + /// URL of the Jellyfin server + /// Environment variable: JELLYFIN_URL + /// + public string? Url { get; set; } + + /// + /// API key for authenticating with Jellyfin server + /// Environment variable: JELLYFIN_API_KEY + /// + public string? ApiKey { get; set; } + + /// + /// User ID for accessing Jellyfin library + /// Environment variable: JELLYFIN_USER_ID + /// + public string? UserId { get; set; } + + /// + /// Username that clients must provide to authenticate + /// Environment variable: JELLYFIN_CLIENT_USERNAME + /// + public string? ClientUsername { get; set; } + + /// + /// Music library ID in Jellyfin (optional, auto-detected if not specified) + /// Environment variable: JELLYFIN_LIBRARY_ID + /// + public string? LibraryId { get; set; } + + /// + /// Client name reported to Jellyfin + /// + public string ClientName { get; set; } = "Allstarr"; + + /// + /// Client version reported to Jellyfin + /// + public string ClientVersion { get; set; } = "1.0.0"; + + /// + /// Device ID reported to Jellyfin + /// + public string DeviceId { get; set; } = "allstarrrr-proxy"; + + /// + /// Device name reported to Jellyfin + /// + public string DeviceName { get; set; } = "Allstarr Proxy"; + + // Shared settings (same as SubsonicSettings) + + public ExplicitFilter ExplicitFilter { get; set; } = ExplicitFilter.All; + public DownloadMode DownloadMode { get; set; } = DownloadMode.Track; + public MusicService MusicService { get; set; } = MusicService.SquidWTF; + public StorageMode StorageMode { get; set; } = StorageMode.Permanent; + public int CacheDurationHours { get; set; } = 1; + public bool EnableExternalPlaylists { get; set; } = true; + public string PlaylistsDirectory { get; set; } = "playlists"; +} diff --git a/octo-fiesta/Models/Settings/QobuzSettings.cs b/allstarr/Models/Settings/QobuzSettings.cs similarity index 94% rename from octo-fiesta/Models/Settings/QobuzSettings.cs rename to allstarr/Models/Settings/QobuzSettings.cs index b1c9956..32a5f26 100644 --- a/octo-fiesta/Models/Settings/QobuzSettings.cs +++ b/allstarr/Models/Settings/QobuzSettings.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Models.Settings; +namespace allstarr.Models.Settings; /// /// Configuration for the Qobuz downloader and metadata service diff --git a/allstarr/Models/Settings/RedisSettings.cs b/allstarr/Models/Settings/RedisSettings.cs new file mode 100644 index 0000000..d118a95 --- /dev/null +++ b/allstarr/Models/Settings/RedisSettings.cs @@ -0,0 +1,7 @@ +namespace allstarr.Models.Settings; + +public class RedisSettings +{ + public bool Enabled { get; set; } = true; + public string ConnectionString { get; set; } = "localhost:6379"; +} diff --git a/octo-fiesta/Models/Settings/SquidWTFSettings.cs b/allstarr/Models/Settings/SquidWTFSettings.cs similarity index 91% rename from octo-fiesta/Models/Settings/SquidWTFSettings.cs rename to allstarr/Models/Settings/SquidWTFSettings.cs index e562900..a6875d1 100644 --- a/octo-fiesta/Models/Settings/SquidWTFSettings.cs +++ b/allstarr/Models/Settings/SquidWTFSettings.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Models.Settings; +namespace allstarr.Models.Settings; /// /// Configuration for the SquidWTF downloader and metadata service diff --git a/octo-fiesta/Models/Settings/SubsonicSettings.cs b/allstarr/Models/Settings/SubsonicSettings.cs similarity index 92% rename from octo-fiesta/Models/Settings/SubsonicSettings.cs rename to allstarr/Models/Settings/SubsonicSettings.cs index dd0647e..c1330bf 100644 --- a/octo-fiesta/Models/Settings/SubsonicSettings.cs +++ b/allstarr/Models/Settings/SubsonicSettings.cs @@ -1,4 +1,20 @@ -namespace octo_fiesta.Models.Settings; +namespace allstarr.Models.Settings; + +/// +/// Media server backend type +/// +public enum BackendType +{ + /// + /// Subsonic-compatible server (Navidrome, Airsonic, etc.) + /// + Subsonic, + + /// + /// Jellyfin media server + /// + Jellyfin +} /// /// Download mode for tracks diff --git a/octo-fiesta/Models/Subsonic/ExternalPlaylist.cs b/allstarr/Models/Subsonic/ExternalPlaylist.cs similarity index 97% rename from octo-fiesta/Models/Subsonic/ExternalPlaylist.cs rename to allstarr/Models/Subsonic/ExternalPlaylist.cs index c86c726..d1db309 100644 --- a/octo-fiesta/Models/Subsonic/ExternalPlaylist.cs +++ b/allstarr/Models/Subsonic/ExternalPlaylist.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Models.Subsonic; +namespace allstarr.Models.Subsonic; /// /// Represents a playlist from an external music provider (Deezer, Qobuz). diff --git a/octo-fiesta/Models/Subsonic/ScanStatus.cs b/allstarr/Models/Subsonic/ScanStatus.cs similarity index 80% rename from octo-fiesta/Models/Subsonic/ScanStatus.cs rename to allstarr/Models/Subsonic/ScanStatus.cs index 03b3343..065a0ce 100644 --- a/octo-fiesta/Models/Subsonic/ScanStatus.cs +++ b/allstarr/Models/Subsonic/ScanStatus.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Models.Subsonic; +namespace allstarr.Models.Subsonic; /// /// Subsonic library scan status diff --git a/allstarr/Program.cs b/allstarr/Program.cs new file mode 100644 index 0000000..8013223 --- /dev/null +++ b/allstarr/Program.cs @@ -0,0 +1,239 @@ +using allstarr.Models.Settings; +using allstarr.Services; +using allstarr.Services.Deezer; +using allstarr.Services.Qobuz; +using allstarr.Services.SquidWTF; +using allstarr.Services.Local; +using allstarr.Services.Validation; +using allstarr.Services.Subsonic; +using allstarr.Services.Jellyfin; +using allstarr.Services.Common; +using allstarr.Middleware; +using allstarr.Filters; + +var builder = WebApplication.CreateBuilder(args); + +// Determine backend type FIRST +var backendType = builder.Configuration.GetValue("Backend:Type"); + +// Configure Kestrel for large responses over VPN/Tailscale +builder.WebHost.ConfigureKestrel(serverOptions => +{ + serverOptions.Limits.MaxResponseBufferSize = null; // Disable response buffering limit + serverOptions.Limits.MaxRequestBodySize = null; // Allow large request bodies + serverOptions.Limits.MinResponseDataRate = null; // Disable minimum data rate for slow connections +}); + +// Add response compression for large JSON responses (helps with Tailscale/VPN MTU issues) +builder.Services.AddResponseCompression(options => +{ + options.EnableForHttps = true; + options.MimeTypes = new[] { "application/json", "text/json" }; +}); + +// Add services to the container - conditionally register controllers +builder.Services.AddControllers() + .AddJsonOptions(options => + { + // Use original property names (PascalCase) to match Jellyfin API + options.JsonSerializerOptions.PropertyNamingPolicy = null; + options.JsonSerializerOptions.DictionaryKeyPolicy = null; + }) + .ConfigureApplicationPartManager(manager => + { + // Remove the default controller feature provider + var defaultProvider = manager.FeatureProviders + .OfType() + .FirstOrDefault(); + if (defaultProvider != null) + { + manager.FeatureProviders.Remove(defaultProvider); + } + // Add our custom provider that filters by backend type + manager.FeatureProviders.Add(new BackendControllerFeatureProvider(backendType)); + }); + +builder.Services.AddHttpClient(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddHttpContextAccessor(); + +// Exception handling +builder.Services.AddExceptionHandler(); +builder.Services.AddProblemDetails(); + +// Configuration - register both settings, active one determined by backend type +builder.Services.Configure( + builder.Configuration.GetSection("Subsonic")); +builder.Services.Configure( + builder.Configuration.GetSection("Jellyfin")); +builder.Services.Configure( + builder.Configuration.GetSection("Deezer")); +builder.Services.Configure( + builder.Configuration.GetSection("Qobuz")); +builder.Services.Configure( + builder.Configuration.GetSection("Redis")); + +// Get shared settings from the active backend config +MusicService musicService; +bool enableExternalPlaylists; + +if (backendType == BackendType.Jellyfin) +{ + musicService = builder.Configuration.GetValue("Jellyfin:MusicService"); + enableExternalPlaylists = builder.Configuration.GetValue("Jellyfin:EnableExternalPlaylists", true); +} +else +{ + // Default to Subsonic + musicService = builder.Configuration.GetValue("Subsonic:MusicService"); + enableExternalPlaylists = builder.Configuration.GetValue("Subsonic:EnableExternalPlaylists", true); +} + +// Business services - shared across backends +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Register backend-specific services +if (backendType == BackendType.Jellyfin) +{ + // Jellyfin services + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); +} +else +{ + // Subsonic services (default) + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); +} + +// Register music service based on configuration +// IMPORTANT: Primary service MUST be registered LAST because ASP.NET Core DI +// will use the last registered implementation when injecting IMusicMetadataService/IDownloadService +if (musicService == MusicService.Qobuz) +{ + // If playlists enabled, register Deezer FIRST (secondary provider) + if (enableExternalPlaylists) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + } + + // Qobuz services (primary) - registered LAST to be injected by default + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); +} +else if (musicService == MusicService.Deezer) +{ + // If playlists enabled, register Qobuz FIRST (secondary provider) + if (enableExternalPlaylists) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + } + + // Deezer services (primary, default) - registered LAST to be injected by default + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); +} +else if (musicService == MusicService.SquidWTF) +{ + // SquidWTF services + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); +} + +// Startup validation - register validators based on backend +if (backendType == BackendType.Jellyfin) +{ + builder.Services.AddSingleton(); +} +else +{ + builder.Services.AddSingleton(); +} + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Register orchestrator as hosted service +builder.Services.AddHostedService(); + +// Register cache cleanup service (only runs when StorageMode is Cache) +builder.Services.AddHostedService(); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .WithExposedHeaders("X-Content-Duration", "X-Total-Count", "X-Nd-Authorization"); + }); +}); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.UseExceptionHandler(_ => { }); // Global exception handler + +// Enable response compression EARLY in the pipeline +app.UseResponseCompression(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.UseCors(); + +app.MapControllers(); + +// Health check endpoint for monitoring +app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow })); + +app.Run(); + +/// +/// Controller feature provider that conditionally registers controllers based on backend type. +/// This prevents route conflicts between JellyfinController and SubsonicController catch-all routes. +/// +class BackendControllerFeatureProvider : Microsoft.AspNetCore.Mvc.Controllers.ControllerFeatureProvider +{ + private readonly BackendType _backendType; + + public BackendControllerFeatureProvider(BackendType backendType) + { + _backendType = backendType; + } + + protected override bool IsController(System.Reflection.TypeInfo typeInfo) + { + var isController = base.IsController(typeInfo); + if (!isController) return false; + + // Only register the controller matching the configured backend type + return _backendType switch + { + BackendType.Jellyfin => typeInfo.Name == "JellyfinController", + BackendType.Subsonic => typeInfo.Name == "SubsonicController", + _ => false + }; + } +} diff --git a/octo-fiesta/Properties/launchSettings.json b/allstarr/Properties/launchSettings.json similarity index 100% rename from octo-fiesta/Properties/launchSettings.json rename to allstarr/Properties/launchSettings.json diff --git a/octo-fiesta/Services/Common/BaseDownloadService.cs b/allstarr/Services/Common/BaseDownloadService.cs similarity index 94% rename from octo-fiesta/Services/Common/BaseDownloadService.cs rename to allstarr/Services/Common/BaseDownloadService.cs index b707d4e..4c2ae84 100644 --- a/octo-fiesta/Services/Common/BaseDownloadService.cs +++ b/allstarr/Services/Common/BaseDownloadService.cs @@ -1,14 +1,14 @@ -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; -using octo_fiesta.Services.Local; -using octo_fiesta.Services.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; +using allstarr.Services.Local; +using allstarr.Services.Subsonic; using TagLib; using IOFile = System.IO.File; -namespace octo_fiesta.Services.Common; +namespace allstarr.Services.Common; /// /// Abstract base class for download services. @@ -89,7 +89,21 @@ public abstract class BaseDownloadService : IDownloadService public async Task DownloadAndStreamAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default) { - var localPath = await DownloadSongInternalAsync(externalProvider, externalId, triggerAlbumDownload: true, cancellationToken); + // Check if already downloaded locally + var localPath = await LocalLibraryService.GetLocalPathForExternalSongAsync(externalProvider, externalId); + if (localPath != null && IOFile.Exists(localPath)) + { + Logger.LogInformation("Streaming from local cache: {Path}", localPath); + return IOFile.OpenRead(localPath); + } + + // For on-demand streaming, download to disk first to ensure complete file + // This is necessary because: + // 1. Clients may seek to arbitrary positions (requires full file) + // 2. Metadata embedding requires complete file + // 3. Caching for future plays + Logger.LogInformation("Downloading song for streaming: {Provider}:{ExternalId}", externalProvider, externalId); + localPath = await DownloadSongInternalAsync(externalProvider, externalId, triggerAlbumDownload: true, cancellationToken); return IOFile.OpenRead(localPath); } diff --git a/octo-fiesta/Services/Common/CacheCleanupService.cs b/allstarr/Services/Common/CacheCleanupService.cs similarity index 98% rename from octo-fiesta/Services/Common/CacheCleanupService.cs rename to allstarr/Services/Common/CacheCleanupService.cs index d49752b..218f5b0 100644 --- a/octo-fiesta/Services/Common/CacheCleanupService.cs +++ b/allstarr/Services/Common/CacheCleanupService.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Options; -using octo_fiesta.Models.Settings; +using allstarr.Models.Settings; -namespace octo_fiesta.Services.Common; +namespace allstarr.Services.Common; /// /// Background service that periodically cleans up old cached files diff --git a/octo-fiesta/Services/Common/Error.cs b/allstarr/Services/Common/Error.cs similarity index 99% rename from octo-fiesta/Services/Common/Error.cs rename to allstarr/Services/Common/Error.cs index aa89217..1aabfe1 100644 --- a/octo-fiesta/Services/Common/Error.cs +++ b/allstarr/Services/Common/Error.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Services.Common; +namespace allstarr.Services.Common; /// /// Represents a typed error with code, message, and metadata diff --git a/allstarr/Services/Common/FuzzyMatcher.cs b/allstarr/Services/Common/FuzzyMatcher.cs new file mode 100644 index 0000000..92664f0 --- /dev/null +++ b/allstarr/Services/Common/FuzzyMatcher.cs @@ -0,0 +1,104 @@ +namespace allstarr.Services.Common; + +/// +/// Provides fuzzy string matching for search result scoring. +/// +public static class FuzzyMatcher +{ + /// + /// Calculates a similarity score between two strings (0-100). + /// Higher score means better match. + /// + public static int CalculateSimilarity(string query, string target) + { + if (string.IsNullOrWhiteSpace(query) || string.IsNullOrWhiteSpace(target)) + { + return 0; + } + + var queryLower = query.ToLowerInvariant().Trim(); + var targetLower = target.ToLowerInvariant().Trim(); + + // Exact match + if (queryLower == targetLower) + { + return 100; + } + + // Starts with query + if (targetLower.StartsWith(queryLower)) + { + return 90; + } + + // Contains query as whole word + if (targetLower.Contains($" {queryLower} ") || + targetLower.StartsWith($"{queryLower} ") || + targetLower.EndsWith($" {queryLower}")) + { + return 80; + } + + // Contains query anywhere + if (targetLower.Contains(queryLower)) + { + return 70; + } + + // Calculate Levenshtein distance for fuzzy matching + var distance = LevenshteinDistance(queryLower, targetLower); + var maxLength = Math.Max(queryLower.Length, targetLower.Length); + + if (maxLength == 0) + { + return 100; + } + + // Convert distance to similarity score (0-60 range for fuzzy matches) + var similarity = (1.0 - (double)distance / maxLength) * 60; + return (int)Math.Max(0, similarity); + } + + /// + /// Calculates Levenshtein distance between two strings. + /// + private static int LevenshteinDistance(string source, string target) + { + if (string.IsNullOrEmpty(source)) + { + return target?.Length ?? 0; + } + + if (string.IsNullOrEmpty(target)) + { + return source.Length; + } + + var sourceLength = source.Length; + var targetLength = target.Length; + var distance = new int[sourceLength + 1, targetLength + 1]; + + for (var i = 0; i <= sourceLength; i++) + { + distance[i, 0] = i; + } + + for (var j = 0; j <= targetLength; j++) + { + distance[0, j] = j; + } + + for (var i = 1; i <= sourceLength; i++) + { + for (var j = 1; j <= targetLength; j++) + { + var cost = target[j - 1] == source[i - 1] ? 0 : 1; + distance[i, j] = Math.Min( + Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1), + distance[i - 1, j - 1] + cost); + } + } + + return distance[sourceLength, targetLength]; + } +} diff --git a/octo-fiesta/Services/Common/PathHelper.cs b/allstarr/Services/Common/PathHelper.cs similarity index 96% rename from octo-fiesta/Services/Common/PathHelper.cs rename to allstarr/Services/Common/PathHelper.cs index 94b226c..35c48f0 100644 --- a/octo-fiesta/Services/Common/PathHelper.cs +++ b/allstarr/Services/Common/PathHelper.cs @@ -1,6 +1,6 @@ using IOFile = System.IO.File; -namespace octo_fiesta.Services.Common; +namespace allstarr.Services.Common; /// /// Helper class for path building and sanitization. @@ -10,13 +10,13 @@ public static class PathHelper { /// /// Gets the cache directory path for temporary file storage. - /// Uses system temp directory combined with octo-fiesta-cache subfolder. + /// Uses system temp directory combined with allstarr-cache subfolder. /// Respects TMPDIR environment variable on Linux/macOS. /// /// Full path to the cache directory. public static string GetCachePath() { - return Path.Combine(Path.GetTempPath(), "octo-fiesta-cache"); + return Path.Combine(Path.GetTempPath(), "allstarr-cache"); } /// diff --git a/octo-fiesta/Services/Common/PlaylistIdHelper.cs b/allstarr/Services/Common/PlaylistIdHelper.cs similarity index 98% rename from octo-fiesta/Services/Common/PlaylistIdHelper.cs rename to allstarr/Services/Common/PlaylistIdHelper.cs index 5440826..2e155bb 100644 --- a/octo-fiesta/Services/Common/PlaylistIdHelper.cs +++ b/allstarr/Services/Common/PlaylistIdHelper.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Services.Common; +namespace allstarr.Services.Common; /// /// Helper class for handling external playlist IDs. diff --git a/allstarr/Services/Common/RedisCacheService.cs b/allstarr/Services/Common/RedisCacheService.cs new file mode 100644 index 0000000..922a2d5 --- /dev/null +++ b/allstarr/Services/Common/RedisCacheService.cs @@ -0,0 +1,157 @@ +using Microsoft.Extensions.Options; +using allstarr.Models.Settings; +using StackExchange.Redis; +using System.Text.Json; + +namespace allstarr.Services.Common; + +/// +/// Redis caching service for metadata and images. +/// +public class RedisCacheService +{ + private readonly RedisSettings _settings; + private readonly ILogger _logger; + private IConnectionMultiplexer? _redis; + private IDatabase? _db; + private readonly object _lock = new(); + + public RedisCacheService( + IOptions settings, + ILogger logger) + { + _settings = settings.Value; + _logger = logger; + + if (_settings.Enabled) + { + InitializeConnection(); + } + } + + private void InitializeConnection() + { + try + { + _redis = ConnectionMultiplexer.Connect(_settings.ConnectionString); + _db = _redis.GetDatabase(); + _logger.LogInformation("Redis connected: {ConnectionString}", _settings.ConnectionString); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Redis connection failed. Caching disabled."); + _redis = null; + _db = null; + } + } + + public bool IsEnabled => _settings.Enabled && _db != null; + + /// + /// Gets a cached value as a string. + /// + public async Task GetStringAsync(string key) + { + if (!IsEnabled) return null; + + try + { + return await _db!.StringGetAsync(key); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Redis GET failed for key: {Key}", key); + return null; + } + } + + /// + /// Gets a cached value and deserializes it. + /// + public async Task GetAsync(string key) where T : class + { + var json = await GetStringAsync(key); + if (string.IsNullOrEmpty(json)) return null; + + try + { + return JsonSerializer.Deserialize(json); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to deserialize cached value for key: {Key}", key); + return null; + } + } + + /// + /// Sets a cached value with TTL. + /// + public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null) + { + if (!IsEnabled) return false; + + try + { + return await _db!.StringSetAsync(key, value, expiry); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Redis SET failed for key: {Key}", key); + return false; + } + } + + /// + /// Sets a cached value by serializing it with TTL. + /// + public async Task SetAsync(string key, T value, TimeSpan? expiry = null) where T : class + { + try + { + var json = JsonSerializer.Serialize(value); + return await SetStringAsync(key, json, expiry); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to serialize value for key: {Key}", key); + return false; + } + } + + /// + /// Deletes a cached value. + /// + public async Task DeleteAsync(string key) + { + if (!IsEnabled) return false; + + try + { + return await _db!.KeyDeleteAsync(key); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Redis DELETE failed for key: {Key}", key); + return false; + } + } + + /// + /// Checks if a key exists. + /// + public async Task ExistsAsync(string key) + { + if (!IsEnabled) return false; + + try + { + return await _db!.KeyExistsAsync(key); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Redis EXISTS failed for key: {Key}", key); + return false; + } + } +} diff --git a/octo-fiesta/Services/Common/Result.cs b/allstarr/Services/Common/Result.cs similarity index 98% rename from octo-fiesta/Services/Common/Result.cs rename to allstarr/Services/Common/Result.cs index 178f758..6a5e4f5 100644 --- a/octo-fiesta/Services/Common/Result.cs +++ b/allstarr/Services/Common/Result.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Services.Common; +namespace allstarr.Services.Common; /// /// Represents the result of an operation that can either succeed with a value or fail with an error. diff --git a/octo-fiesta/Services/Deezer/DeezerDownloadService.cs b/allstarr/Services/Deezer/DeezerDownloadService.cs similarity index 98% rename from octo-fiesta/Services/Deezer/DeezerDownloadService.cs rename to allstarr/Services/Deezer/DeezerDownloadService.cs index 53be347..bf0240e 100644 --- a/octo-fiesta/Services/Deezer/DeezerDownloadService.cs +++ b/allstarr/Services/Deezer/DeezerDownloadService.cs @@ -4,18 +4,18 @@ using System.Text.Json; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Parameters; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; -using octo_fiesta.Services.Local; -using octo_fiesta.Services.Common; -using octo_fiesta.Services.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; +using allstarr.Services.Local; +using allstarr.Services.Common; +using allstarr.Services.Subsonic; using Microsoft.Extensions.Options; using IOFile = System.IO.File; -namespace octo_fiesta.Services.Deezer; +namespace allstarr.Services.Deezer; /// /// C# port of the DeezerDownloader JavaScript diff --git a/octo-fiesta/Services/Deezer/DeezerMetadataService.cs b/allstarr/Services/Deezer/DeezerMetadataService.cs similarity index 99% rename from octo-fiesta/Services/Deezer/DeezerMetadataService.cs rename to allstarr/Services/Deezer/DeezerMetadataService.cs index 33b14fe..ae5aa3e 100644 --- a/octo-fiesta/Services/Deezer/DeezerMetadataService.cs +++ b/allstarr/Services/Deezer/DeezerMetadataService.cs @@ -1,12 +1,12 @@ -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; using System.Text.Json; using Microsoft.Extensions.Options; -namespace octo_fiesta.Services.Deezer; +namespace allstarr.Services.Deezer; /// /// Metadata service implementation using the Deezer API (free, no key required) diff --git a/octo-fiesta/Services/Deezer/DeezerStartupValidator.cs b/allstarr/Services/Deezer/DeezerStartupValidator.cs similarity index 98% rename from octo-fiesta/Services/Deezer/DeezerStartupValidator.cs rename to allstarr/Services/Deezer/DeezerStartupValidator.cs index 0f3e4d1..91d6f3a 100644 --- a/octo-fiesta/Services/Deezer/DeezerStartupValidator.cs +++ b/allstarr/Services/Deezer/DeezerStartupValidator.cs @@ -1,10 +1,10 @@ using System.Text; using System.Text.Json; using Microsoft.Extensions.Options; -using octo_fiesta.Models.Settings; -using octo_fiesta.Services.Validation; +using allstarr.Models.Settings; +using allstarr.Services.Validation; -namespace octo_fiesta.Services.Deezer; +namespace allstarr.Services.Deezer; /// /// Validates Deezer ARL credentials at startup diff --git a/octo-fiesta/Services/IDownloadService.cs b/allstarr/Services/IDownloadService.cs similarity index 92% rename from octo-fiesta/Services/IDownloadService.cs rename to allstarr/Services/IDownloadService.cs index 9c28ff0..1f95f76 100644 --- a/octo-fiesta/Services/IDownloadService.cs +++ b/allstarr/Services/IDownloadService.cs @@ -1,10 +1,10 @@ -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; -namespace octo_fiesta.Services; +namespace allstarr.Services; /// /// Interface for the music download service (Deezspot or other) diff --git a/octo-fiesta/Services/IMusicMetadataService.cs b/allstarr/Services/IMusicMetadataService.cs similarity index 93% rename from octo-fiesta/Services/IMusicMetadataService.cs rename to allstarr/Services/IMusicMetadataService.cs index eabdf28..d32f715 100644 --- a/octo-fiesta/Services/IMusicMetadataService.cs +++ b/allstarr/Services/IMusicMetadataService.cs @@ -1,10 +1,10 @@ -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; -namespace octo_fiesta.Services; +namespace allstarr.Services; /// /// Interface for external music metadata search service diff --git a/allstarr/Services/Jellyfin/JellyfinModelMapper.cs b/allstarr/Services/Jellyfin/JellyfinModelMapper.cs new file mode 100644 index 0000000..6ea1502 --- /dev/null +++ b/allstarr/Services/Jellyfin/JellyfinModelMapper.cs @@ -0,0 +1,385 @@ +using System.Text.Json; +using allstarr.Models.Domain; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; + +namespace allstarr.Services.Jellyfin; + +/// +/// Maps between Jellyfin API responses and domain models. +/// +public class JellyfinModelMapper +{ + private readonly JellyfinResponseBuilder _responseBuilder; + private readonly ILogger _logger; + + public JellyfinModelMapper( + JellyfinResponseBuilder responseBuilder, + ILogger logger) + { + _responseBuilder = responseBuilder; + _logger = logger; + } + + /// + /// Parses a Jellyfin items response into domain objects. + /// + public (List Songs, List Albums, List Artists) ParseItemsResponse(JsonDocument? response) + { + var songs = new List(); + var albums = new List(); + var artists = new List(); + + if (response == null) + { + return (songs, albums, artists); + } + + try + { + JsonElement items; + + // Handle both direct array and Items property + if (response.RootElement.TryGetProperty("Items", out items)) + { + // Standard items response + } + else if (response.RootElement.ValueKind == JsonValueKind.Array) + { + items = response.RootElement; + } + else + { + return (songs, albums, artists); + } + + foreach (var item in items.EnumerateArray()) + { + var type = item.TryGetProperty("Type", out var typeEl) + ? typeEl.GetString() + : null; + + switch (type) + { + case "Audio": + songs.Add(ParseSong(item)); + break; + case "MusicAlbum": + albums.Add(ParseAlbum(item)); + break; + case "MusicArtist": + case "Artist": + artists.Add(ParseArtist(item)); + break; + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error parsing Jellyfin items response"); + } + + return (songs, albums, artists); + } + + /// + /// Parses a Jellyfin search hints response. + /// + public (List Songs, List Albums, List Artists) ParseSearchHintsResponse(JsonDocument? response) + { + var songs = new List(); + var albums = new List(); + var artists = new List(); + + if (response == null) + { + return (songs, albums, artists); + } + + try + { + if (!response.RootElement.TryGetProperty("SearchHints", out var hints)) + { + return (songs, albums, artists); + } + + foreach (var hint in hints.EnumerateArray()) + { + var type = hint.TryGetProperty("Type", out var typeEl) + ? typeEl.GetString() + : null; + + switch (type) + { + case "Audio": + songs.Add(ParseSongFromHint(hint)); + break; + case "MusicAlbum": + albums.Add(ParseAlbumFromHint(hint)); + break; + case "MusicArtist": + case "Artist": + artists.Add(ParseArtistFromHint(hint)); + break; + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error parsing Jellyfin search hints response"); + } + + return (songs, albums, artists); + } + + /// + /// Parses a single Jellyfin item as a Song. + /// + public Song ParseSong(JsonElement item) + { + var id = item.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; + var runTimeTicks = item.TryGetProperty("RunTimeTicks", out var rtt) ? rtt.GetInt64() : 0; + + var song = new Song + { + Id = id, + Title = item.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", + Album = item.TryGetProperty("Album", out var album) ? album.GetString() ?? "" : "", + AlbumId = item.TryGetProperty("AlbumId", out var albumId) ? albumId.GetString() : null, + Duration = (int)(runTimeTicks / TimeSpan.TicksPerSecond), + Track = item.TryGetProperty("IndexNumber", out var track) ? track.GetInt32() : null, + DiscNumber = item.TryGetProperty("ParentIndexNumber", out var disc) ? disc.GetInt32() : null, + Year = item.TryGetProperty("ProductionYear", out var year) ? year.GetInt32() : null, + IsLocal = true + }; + + // Get artist info + if (item.TryGetProperty("Artists", out var artists) && artists.GetArrayLength() > 0) + { + song.Artist = artists[0].GetString() ?? ""; + } + else if (item.TryGetProperty("AlbumArtist", out var albumArtist)) + { + song.Artist = albumArtist.GetString() ?? ""; + } + + if (item.TryGetProperty("ArtistItems", out var artistItems) && artistItems.GetArrayLength() > 0) + { + var firstArtist = artistItems[0]; + song.ArtistId = firstArtist.TryGetProperty("Id", out var artId) ? artId.GetString() : null; + } + + // Get genre + if (item.TryGetProperty("Genres", out var genres) && genres.GetArrayLength() > 0) + { + song.Genre = genres[0].GetString(); + } + + // Get provider IDs + if (item.TryGetProperty("ProviderIds", out var providerIds)) + { + if (providerIds.TryGetProperty("ISRC", out var isrc)) + { + song.Isrc = isrc.GetString(); + } + } + + // Cover art URL construction + song.CoverArtUrl = $"/Items/{id}/Images/Primary"; + + return song; + } + + /// + /// Parses a search hint as a Song. + /// + private Song ParseSongFromHint(JsonElement hint) + { + var id = hint.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; + var runTimeTicks = hint.TryGetProperty("RunTimeTicks", out var rtt) ? rtt.GetInt64() : 0; + + return new Song + { + Id = id, + Title = hint.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", + Album = hint.TryGetProperty("Album", out var album) ? album.GetString() ?? "" : "", + Artist = hint.TryGetProperty("AlbumArtist", out var artist) ? artist.GetString() ?? "" : "", + Duration = (int)(runTimeTicks / TimeSpan.TicksPerSecond), + IsLocal = true, + CoverArtUrl = $"/Items/{id}/Images/Primary" + }; + } + + /// + /// Parses a single Jellyfin item as an Album. + /// + public Album ParseAlbum(JsonElement item) + { + var id = item.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; + + var album = new Album + { + Id = id, + Title = item.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", + Artist = item.TryGetProperty("AlbumArtist", out var artist) ? artist.GetString() ?? "" : "", + Year = item.TryGetProperty("ProductionYear", out var year) ? year.GetInt32() : null, + SongCount = item.TryGetProperty("ChildCount", out var count) ? count.GetInt32() : null, + IsLocal = true, + CoverArtUrl = $"/Items/{id}/Images/Primary" + }; + + // Get artist ID + if (item.TryGetProperty("AlbumArtists", out var albumArtists) && albumArtists.GetArrayLength() > 0) + { + var firstArtist = albumArtists[0]; + album.ArtistId = firstArtist.TryGetProperty("Id", out var artId) ? artId.GetString() : null; + } + + // Get genre + if (item.TryGetProperty("Genres", out var genres) && genres.GetArrayLength() > 0) + { + album.Genre = genres[0].GetString(); + } + + return album; + } + + /// + /// Parses a search hint as an Album. + /// + private Album ParseAlbumFromHint(JsonElement hint) + { + var id = hint.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; + + return new Album + { + Id = id, + Title = hint.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", + Artist = hint.TryGetProperty("AlbumArtist", out var artist) ? artist.GetString() ?? "" : "", + Year = hint.TryGetProperty("ProductionYear", out var year) ? year.GetInt32() : null, + IsLocal = true, + CoverArtUrl = $"/Items/{id}/Images/Primary" + }; + } + + /// + /// Parses a single Jellyfin item as an Artist. + /// + public Artist ParseArtist(JsonElement item) + { + var id = item.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; + + return new Artist + { + Id = id, + Name = item.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", + AlbumCount = item.TryGetProperty("AlbumCount", out var count) ? count.GetInt32() : null, + IsLocal = true, + ImageUrl = $"/Items/{id}/Images/Primary" + }; + } + + /// + /// Parses a search hint as an Artist. + /// + private Artist ParseArtistFromHint(JsonElement hint) + { + var id = hint.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; + + return new Artist + { + Id = id, + Name = hint.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", + IsLocal = true, + ImageUrl = $"/Items/{id}/Images/Primary" + }; + } + + /// + /// Merges local Jellyfin results with external search results. + /// + public (List> MergedSongs, + List> MergedAlbums, + List> MergedArtists) MergeSearchResults( + List localSongs, + List localAlbums, + List localArtists, + SearchResult externalResult, + List externalPlaylists) + { + // Convert local results to Jellyfin format + var mergedSongs = localSongs + .Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)) + .Concat(externalResult.Songs.Select(s => _responseBuilder.ConvertSongToJellyfinItem(s))) + .ToList(); + + // Merge albums with playlists + var mergedAlbums = localAlbums + .Select(a => _responseBuilder.ConvertAlbumToJellyfinItem(a)) + .Concat(externalResult.Albums.Select(a => _responseBuilder.ConvertAlbumToJellyfinItem(a))) + .Concat(externalPlaylists.Select(p => _responseBuilder.ConvertPlaylistToAlbumItem(p))) + .ToList(); + + // Deduplicate artists by name - prefer local artists + var localArtistNames = new HashSet( + localArtists.Select(a => a.Name), + StringComparer.OrdinalIgnoreCase); + + var mergedArtists = localArtists + .Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)) + .ToList(); + + foreach (var externalArtist in externalResult.Artists) + { + if (!localArtistNames.Contains(externalArtist.Name)) + { + mergedArtists.Add(_responseBuilder.ConvertArtistToJellyfinItem(externalArtist)); + } + } + + return (mergedSongs, mergedAlbums, mergedArtists); + } + + /// + /// Parses an album with its tracks from a Jellyfin response. + /// + public Album? ParseAlbumWithTracks(JsonDocument? albumResponse, JsonDocument? tracksResponse) + { + if (albumResponse == null) + { + return null; + } + + var album = ParseAlbum(albumResponse.RootElement); + + if (tracksResponse != null && tracksResponse.RootElement.TryGetProperty("Items", out var tracks)) + { + foreach (var track in tracks.EnumerateArray()) + { + album.Songs.Add(ParseSong(track)); + } + } + + return album; + } + + /// + /// Parses an artist with albums from Jellyfin responses. + /// + public Artist? ParseArtistWithAlbums(JsonDocument? artistResponse, JsonDocument? albumsResponse) + { + if (artistResponse == null) + { + return null; + } + + var artist = ParseArtist(artistResponse.RootElement); + + if (albumsResponse != null && albumsResponse.RootElement.TryGetProperty("Items", out var albums)) + { + artist.AlbumCount = albums.GetArrayLength(); + } + + return artist; + } +} diff --git a/allstarr/Services/Jellyfin/JellyfinProxyService.cs b/allstarr/Services/Jellyfin/JellyfinProxyService.cs new file mode 100644 index 0000000..75df0fb --- /dev/null +++ b/allstarr/Services/Jellyfin/JellyfinProxyService.cs @@ -0,0 +1,698 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using allstarr.Models.Settings; +using allstarr.Services.Common; +using System.Net.Http.Headers; +using System.Text.Json; + +namespace allstarr.Services.Jellyfin; + +/// +/// Handles proxying requests to the Jellyfin server and authentication. +/// +public class JellyfinProxyService +{ + private readonly HttpClient _httpClient; + private readonly JellyfinSettings _settings; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger _logger; + private readonly RedisCacheService _cache; + private string? _cachedMusicLibraryId; + private bool _libraryIdDetected = false; + + // Expose HttpClient for direct streaming scenarios + public HttpClient HttpClient => _httpClient; + + public JellyfinProxyService( + IHttpClientFactory httpClientFactory, + IOptions settings, + IHttpContextAccessor httpContextAccessor, + ILogger logger, + RedisCacheService cache) + { + _httpClient = httpClientFactory.CreateClient(); + _settings = settings.Value; + _httpContextAccessor = httpContextAccessor; + _logger = logger; + _cache = cache; + } + + /// + /// Gets the music library ID, auto-detecting it if not configured. + /// + private async Task GetMusicLibraryIdAsync() + { + // Return configured library ID if set + if (!string.IsNullOrEmpty(_settings.LibraryId)) + { + return _settings.LibraryId; + } + + // Return cached value if already detected + if (_libraryIdDetected) + { + return _cachedMusicLibraryId; + } + + // Auto-detect music library ID + try + { + _logger.LogInformation("Auto-detecting music library ID..."); + _cachedMusicLibraryId = await GetMusicLibraryIdInternalAsync(); + _libraryIdDetected = true; + + if (!string.IsNullOrEmpty(_cachedMusicLibraryId)) + { + _logger.LogInformation("Music library auto-detected: {LibraryId}", _cachedMusicLibraryId); + } + else + { + _logger.LogWarning("Could not auto-detect music library. All content types will be visible. Set JELLYFIN_LIBRARY_ID to filter to music only."); + } + + return _cachedMusicLibraryId; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to auto-detect music library ID"); + _libraryIdDetected = true; // Don't keep trying + return null; + } + } + + /// + /// Public method for controllers to get the music library ID for filtering. + /// + public async Task GetMusicLibraryIdForFilteringAsync() + { + return await GetMusicLibraryIdAsync(); + } + + /// + /// Gets the authorization header value for Jellyfin API requests. + /// + private string GetAuthorizationHeader() + { + return $"MediaBrowser Client=\"{_settings.ClientName}\", " + + $"Device=\"{_settings.DeviceName}\", " + + $"DeviceId=\"{_settings.DeviceId}\", " + + $"Version=\"{_settings.ClientVersion}\", " + + $"Token=\"{_settings.ApiKey}\""; + } + + /// + /// Sends a GET request to the Jellyfin server. + /// If endpoint already contains query parameters, they will be preserved and merged with queryParams. + /// + public async Task GetJsonAsync(string endpoint, Dictionary? queryParams = null, IHeaderDictionary? clientHeaders = null) + { + // If endpoint contains query string, parse and merge with queryParams + if (endpoint.Contains('?')) + { + var parts = endpoint.Split('?', 2); + var baseEndpoint = parts[0]; + var existingQuery = parts[1]; + + // Parse existing query string + var mergedParams = new Dictionary(); + foreach (var param in existingQuery.Split('&')) + { + var kv = param.Split('=', 2); + if (kv.Length == 2) + { + mergedParams[Uri.UnescapeDataString(kv[0])] = Uri.UnescapeDataString(kv[1]); + } + } + + // Merge with provided queryParams (provided params take precedence) + if (queryParams != null) + { + foreach (var kv in queryParams) + { + mergedParams[kv.Key] = kv.Value; + } + } + + var url = BuildUrl(baseEndpoint, mergedParams); + return await GetJsonAsyncInternal(url, clientHeaders); + } + + var finalUrl = BuildUrl(endpoint, queryParams); + return await GetJsonAsyncInternal(finalUrl, clientHeaders); + } + + private async Task GetJsonAsyncInternal(string url, IHeaderDictionary? clientHeaders) + { + using var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Forward authentication headers from client if provided + if (clientHeaders != null) + { + if (clientHeaders.TryGetValue("X-Emby-Authorization", out var embyAuth)) + { + request.Headers.TryAddWithoutValidation("X-Emby-Authorization", embyAuth.ToString()); + } + else if (clientHeaders.TryGetValue("Authorization", out var auth)) + { + request.Headers.TryAddWithoutValidation("Authorization", auth.ToString()); + } + } + + // Only use API key for server-initiated requests (when no client headers provided) + // This ensures client requests use the logged-in user's permissions + if (clientHeaders == null && !request.Headers.Contains("X-Emby-Authorization") && !request.Headers.Contains("Authorization")) + { + if (!string.IsNullOrEmpty(_settings.ApiKey)) + { + request.Headers.Add("Authorization", GetAuthorizationHeader()); + } + } + + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await _httpClient.SendAsync(request); + + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Jellyfin request failed: {StatusCode} for {Url}", response.StatusCode, url); + return null; + } + + var content = await response.Content.ReadAsStringAsync(); + return JsonDocument.Parse(content); + } + + /// + /// Sends a POST request to the Jellyfin server with JSON body. + /// Forwards client headers for authentication passthrough. + /// + public async Task PostJsonAsync(string endpoint, string body, IHeaderDictionary clientHeaders) + { + var url = BuildUrl(endpoint, null); + + using var request = new HttpRequestMessage(HttpMethod.Post, url); + + // Create content from body string + if (!string.IsNullOrEmpty(body)) + { + request.Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json"); + _logger.LogDebug("POST body length: {Length} bytes", body.Length); + } + else + { + _logger.LogWarning("POST body is empty for {Url}", url); + } + + // For auth endpoints, we need X-Emby-Authorization header with client info (no token yet) + // Jellyfin requires this header format even for login + if (clientHeaders.TryGetValue("X-Emby-Authorization", out var embyAuth)) + { + request.Headers.TryAddWithoutValidation("X-Emby-Authorization", embyAuth.ToString()); + _logger.LogDebug("Forwarding X-Emby-Authorization: {Header}", embyAuth.ToString()); + } + else if (clientHeaders.TryGetValue("Authorization", out var auth)) + { + request.Headers.TryAddWithoutValidation("Authorization", auth.ToString()); + _logger.LogDebug("Forwarding Authorization: {Header}", auth.ToString()); + } + else + { + // For login requests, provide a minimal client auth header (no token) + var clientAuthHeader = $"MediaBrowser Client=\"{_settings.ClientName}\", " + + $"Device=\"{_settings.DeviceName}\", " + + $"DeviceId=\"{_settings.DeviceId}\", " + + $"Version=\"{_settings.ClientVersion}\""; + request.Headers.TryAddWithoutValidation("X-Emby-Authorization", clientAuthHeader); + _logger.LogDebug("Using default X-Emby-Authorization for login: {Header}", clientAuthHeader); + } + + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + // DO NOT log the body for auth endpoints - it contains passwords! + if (endpoint.Contains("Authenticate", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("POST to Jellyfin: {Url} (auth request - body not logged)", url); + } + else + { + _logger.LogInformation("POST to Jellyfin: {Url}, body length: {Length} bytes", url, body.Length); + + // Log body content for playback endpoints to debug + if (endpoint.Contains("Playing", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Sending body to Jellyfin: {Body}", body); + } + } + + var response = await _httpClient.SendAsync(request); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogWarning("Jellyfin POST request failed: {StatusCode} for {Url}. Response: {Response}", + response.StatusCode, url, errorContent); + return null; + } + + // Handle 204 No Content responses (e.g., /sessions/playing, /sessions/playing/progress) + if (response.StatusCode == System.Net.HttpStatusCode.NoContent) + { + return null; + } + + var responseContent = await response.Content.ReadAsStringAsync(); + + // Handle empty responses + if (string.IsNullOrWhiteSpace(responseContent)) + { + return null; + } + + return JsonDocument.Parse(responseContent); + } + + /// + /// Sends a GET request and returns raw bytes (for images, audio streams). + /// + public async Task<(byte[] Body, string? ContentType)> GetBytesAsync(string endpoint, Dictionary? queryParams = null) + { + var url = BuildUrl(endpoint, queryParams); + + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Authorization", GetAuthorizationHeader()); + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var body = await response.Content.ReadAsByteArrayAsync(); + var contentType = response.Content.Headers.ContentType?.ToString(); + + return (body, contentType); + } + + /// + /// Safely sends a GET request to the Jellyfin server, returning null on failure. + /// + public async Task<(byte[]? Body, string? ContentType, bool Success)> GetBytesSafeAsync( + string endpoint, + Dictionary? queryParams = null) + { + try + { + var result = await GetBytesAsync(endpoint, queryParams); + return (result.Body, result.ContentType, true); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to get bytes from {Endpoint}", endpoint); + return (null, null, false); + } + } + + /// + /// Searches for items in Jellyfin. + /// Uses configured or auto-detected LibraryId to filter search to music library only. + /// + public async Task SearchAsync( + string searchTerm, + string[]? includeItemTypes = null, + int limit = 20, + bool recursive = true, + IHeaderDictionary? clientHeaders = null) + { + var queryParams = new Dictionary + { + ["searchTerm"] = searchTerm, + ["limit"] = limit.ToString(), + ["recursive"] = recursive.ToString().ToLower(), + ["fields"] = "PrimaryImageAspectRatio,MediaSources,Path,Genres,Studios,DateCreated,Overview,ProviderIds" + }; + + if (!string.IsNullOrEmpty(_settings.UserId)) + { + queryParams["userId"] = _settings.UserId; + } + + // Only filter search to music library if explicitly configured + if (!string.IsNullOrEmpty(_settings.LibraryId)) + { + queryParams["parentId"] = _settings.LibraryId; + _logger.LogDebug("Searching within configured LibraryId {LibraryId}", _settings.LibraryId); + } + + if (includeItemTypes != null && includeItemTypes.Length > 0) + { + queryParams["includeItemTypes"] = string.Join(",", includeItemTypes); + } + + return await GetJsonAsync("Items", queryParams, clientHeaders); + } + + /// + /// Gets items from a specific parent (album, artist, playlist). + /// + public async Task GetItemsAsync( + string? parentId = null, + string[]? includeItemTypes = null, + string? sortBy = null, + int? limit = null, + int? startIndex = null, + IHeaderDictionary? clientHeaders = null) + { + var queryParams = new Dictionary + { + ["recursive"] = "true", + ["fields"] = "PrimaryImageAspectRatio,MediaSources,Path,Genres,Studios,DateCreated,Overview,ProviderIds,ParentId" + }; + + if (!string.IsNullOrEmpty(_settings.UserId)) + { + queryParams["userId"] = _settings.UserId; + } + + if (!string.IsNullOrEmpty(parentId)) + { + queryParams["parentId"] = parentId; + } + + if (includeItemTypes != null && includeItemTypes.Length > 0) + { + queryParams["includeItemTypes"] = string.Join(",", includeItemTypes); + } + + if (!string.IsNullOrEmpty(sortBy)) + { + queryParams["sortBy"] = sortBy; + } + + if (limit.HasValue) + { + queryParams["limit"] = limit.Value.ToString(); + } + + if (startIndex.HasValue) + { + queryParams["startIndex"] = startIndex.Value.ToString(); + } + + return await GetJsonAsync("Items", queryParams, clientHeaders); + } + + /// + /// Gets a single item by ID. + /// + public async Task GetItemAsync(string itemId, IHeaderDictionary? clientHeaders = null) + { + var queryParams = new Dictionary(); + + if (!string.IsNullOrEmpty(_settings.UserId)) + { + queryParams["userId"] = _settings.UserId; + } + + return await GetJsonAsync($"Items/{itemId}", queryParams, clientHeaders); + } + + /// + /// Gets artists from the library. + /// + public async Task GetArtistsAsync( + string? searchTerm = null, + int? limit = null, + int? startIndex = null, + IHeaderDictionary? clientHeaders = null) + { + var queryParams = new Dictionary + { + ["fields"] = "PrimaryImageAspectRatio,Genres,Overview" + }; + + if (!string.IsNullOrEmpty(_settings.UserId)) + { + queryParams["userId"] = _settings.UserId; + } + + if (!string.IsNullOrEmpty(searchTerm)) + { + queryParams["searchTerm"] = searchTerm; + } + + if (limit.HasValue) + { + queryParams["limit"] = limit.Value.ToString(); + } + + if (startIndex.HasValue) + { + queryParams["startIndex"] = startIndex.Value.ToString(); + } + + return await GetJsonAsync("Artists", queryParams, clientHeaders); + } + + /// + /// Gets an artist by name or ID. + /// + public async Task GetArtistAsync(string artistIdOrName, IHeaderDictionary? clientHeaders = null) + { + var queryParams = new Dictionary(); + + if (!string.IsNullOrEmpty(_settings.UserId)) + { + queryParams["userId"] = _settings.UserId; + } + + // Try to get by ID first + if (Guid.TryParse(artistIdOrName, out _)) + { + return await GetJsonAsync($"Items/{artistIdOrName}", queryParams, clientHeaders); + } + + // Otherwise search by name + return await GetJsonAsync($"Artists/{Uri.EscapeDataString(artistIdOrName)}", queryParams, clientHeaders); + } + + /// + /// Streams audio from Jellyfin with range support. + /// + public async Task StreamAudioAsync( + string itemId, + CancellationToken cancellationToken) + { + try + { + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) + { + return new ObjectResult(new { error = "HTTP context not available" }) + { + StatusCode = 500 + }; + } + + var incomingRequest = httpContext.Request; + var outgoingResponse = httpContext.Response; + + // Build the stream URL - use static streaming for simplicity + var queryParams = new Dictionary + { + ["static"] = "true", + ["mediaSourceId"] = itemId + }; + + var url = BuildUrl($"Audio/{itemId}/stream", queryParams); + + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Authorization", GetAuthorizationHeader()); + + // Forward Range headers for progressive streaming + if (incomingRequest.Headers.TryGetValue("Range", out var range)) + { + request.Headers.TryAddWithoutValidation("Range", range.ToArray()); + } + + if (incomingRequest.Headers.TryGetValue("If-Range", out var ifRange)) + { + request.Headers.TryAddWithoutValidation("If-Range", ifRange.ToArray()); + } + + var response = await _httpClient.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken); + + if (!response.IsSuccessStatusCode) + { + return new StatusCodeResult((int)response.StatusCode); + } + + // Forward HTTP status code + outgoingResponse.StatusCode = (int)response.StatusCode; + + // Forward streaming headers + var streamingHeaders = new[] { "Accept-Ranges", "Content-Range", "Content-Length", "ETag", "Last-Modified" }; + foreach (var header in streamingHeaders) + { + if (response.Headers.TryGetValues(header, out var values) || + response.Content.Headers.TryGetValues(header, out values)) + { + outgoingResponse.Headers[header] = values.ToArray(); + } + } + + var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + var contentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg"; + + return new FileStreamResult(stream, contentType) + { + EnableRangeProcessing = true + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error streaming from Jellyfin item {ItemId}", itemId); + return new ObjectResult(new { error = $"Error streaming: {ex.Message}" }) + { + StatusCode = 500 + }; + } + } + + /// + /// Gets the image for an item. + /// + public async Task<(byte[]? Body, string? ContentType)> GetImageAsync( + string itemId, + string imageType = "Primary", + int? maxWidth = null, + int? maxHeight = null) + { + // Build cache key + var cacheKey = $"image:{itemId}:{imageType}:{maxWidth}:{maxHeight}"; + + // Try cache first + var cached = await _cache.GetStringAsync(cacheKey); + if (!string.IsNullOrEmpty(cached)) + { + var parts = cached.Split('|', 2); + if (parts.Length == 2) + { + var body = Convert.FromBase64String(parts[0]); + var contentType = parts[1]; + return (body, contentType); + } + } + + var queryParams = new Dictionary(); + + if (maxWidth.HasValue) + { + queryParams["maxWidth"] = maxWidth.Value.ToString(); + } + + if (maxHeight.HasValue) + { + queryParams["maxHeight"] = maxHeight.Value.ToString(); + } + + var result = await GetBytesSafeAsync($"Items/{itemId}/Images/{imageType}", queryParams); + + // Cache for 7 days if successful + if (result.Success && result.Body != null) + { + var cacheValue = $"{Convert.ToBase64String(result.Body)}|{result.ContentType}"; + await _cache.SetStringAsync(cacheKey, cacheValue, TimeSpan.FromDays(7)); + } + + return (result.Body, result.ContentType); + } + + /// + /// Tests connection to the Jellyfin server. + /// + public async Task<(bool Success, string? ServerName, string? Version)> TestConnectionAsync() + { + try + { + var result = await GetJsonAsync("System/Info/Public"); + if (result == null) + { + return (false, null, null); + } + + var serverName = result.RootElement.TryGetProperty("ServerName", out var name) + ? name.GetString() + : null; + var version = result.RootElement.TryGetProperty("Version", out var ver) + ? ver.GetString() + : null; + + return (true, serverName, version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to test Jellyfin connection"); + return (false, null, null); + } + } + + /// + /// Gets the music library ID from Jellyfin by querying media folders. + /// + private async Task GetMusicLibraryIdInternalAsync() + { + try + { + var queryParams = new Dictionary(); + if (!string.IsNullOrEmpty(_settings.UserId)) + { + queryParams["userId"] = _settings.UserId; + } + + var result = await GetJsonAsync("Library/MediaFolders", queryParams); + if (result == null) + { + return null; + } + + if (result.RootElement.TryGetProperty("Items", out var items)) + { + foreach (var item in items.EnumerateArray()) + { + var collectionType = item.TryGetProperty("CollectionType", out var ct) + ? ct.GetString() + : null; + + if (collectionType == "music") + { + return item.TryGetProperty("Id", out var id) + ? id.GetString() + : null; + } + } + } + + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get music library ID"); + return null; + } + } + + private string BuildUrl(string endpoint, Dictionary? queryParams = null) + { + var baseUrl = _settings.Url?.TrimEnd('/') ?? ""; + var url = $"{baseUrl}/{endpoint}"; + + if (queryParams != null && queryParams.Count > 0) + { + var query = string.Join("&", queryParams.Select(kv => + $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); + url = $"{url}?{query}"; + } + + return url; + } +} diff --git a/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs new file mode 100644 index 0000000..7d67a06 --- /dev/null +++ b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs @@ -0,0 +1,513 @@ +using Microsoft.AspNetCore.Mvc; +using System.Text.Json; +using allstarr.Models.Domain; +using allstarr.Models.Subsonic; + +namespace allstarr.Services.Jellyfin; + +/// +/// Builds Jellyfin-compatible API responses. +/// +public class JellyfinResponseBuilder +{ + /// + /// Creates a Jellyfin items response containing songs. + /// + public IActionResult CreateItemsResponse(List songs) + { + var items = songs.Select(ConvertSongToJellyfinItem).ToList(); + + return CreateJsonResponse(new + { + Items = items, + TotalRecordCount = items.Count, + StartIndex = 0 + }); + } + + /// + /// Creates a Jellyfin items response for albums. + /// + public IActionResult CreateAlbumsResponse(List albums) + { + var items = albums.Select(ConvertAlbumToJellyfinItem).ToList(); + + return CreateJsonResponse(new + { + Items = items, + TotalRecordCount = items.Count, + StartIndex = 0 + }); + } + + /// + /// Creates a Jellyfin items response for artists. + /// + public IActionResult CreateArtistsResponse(List artists) + { + var items = artists.Select(ConvertArtistToJellyfinItem).ToList(); + + return CreateJsonResponse(new + { + Items = items, + TotalRecordCount = items.Count, + StartIndex = 0 + }); + } + + /// + /// Creates a single item response. + /// + public IActionResult CreateSongResponse(Song song) + { + return CreateJsonResponse(ConvertSongToJellyfinItem(song)); + } + + /// + /// Creates a single album response with tracks. + /// + public IActionResult CreateAlbumResponse(Album album) + { + var albumItem = ConvertAlbumToJellyfinItem(album); + + // For album detail, include child items (songs) + if (album.Songs.Count > 0) + { + albumItem["Children"] = album.Songs.Select(ConvertSongToJellyfinItem).ToList(); + } + + return CreateJsonResponse(albumItem); + } + + /// + /// Creates a single artist response with albums. + /// + public IActionResult CreateArtistResponse(Artist artist, List albums) + { + var artistItem = ConvertArtistToJellyfinItem(artist); + artistItem["Albums"] = albums.Select(ConvertAlbumToJellyfinItem).ToList(); + + return CreateJsonResponse(artistItem); + } + + /// + /// Creates a response for a playlist represented as an album. + /// + public IActionResult CreatePlaylistAsAlbumResponse(ExternalPlaylist playlist, List tracks) + { + var totalDuration = tracks.Sum(s => s.Duration ?? 0); + + var curatorName = !string.IsNullOrEmpty(playlist.CuratorName) + ? playlist.CuratorName + : playlist.Provider; + + var albumItem = new Dictionary + { + ["Id"] = playlist.Id, + ["Name"] = playlist.Name, + ["Type"] = "Playlist", + ["AlbumArtist"] = curatorName, + ["Genres"] = new[] { "Playlist" }, + ["ChildCount"] = tracks.Count, + ["RunTimeTicks"] = totalDuration * TimeSpan.TicksPerSecond, + ["ImageTags"] = new Dictionary + { + ["Primary"] = playlist.Id + }, + ["ProviderIds"] = new Dictionary + { + [playlist.Provider] = playlist.ExternalId + }, + ["Children"] = tracks.Select(ConvertSongToJellyfinItem).ToList() + }; + + if (playlist.CreatedDate.HasValue) + { + albumItem["PremiereDate"] = playlist.CreatedDate.Value.ToString("o"); + albumItem["ProductionYear"] = playlist.CreatedDate.Value.Year; + } + + return CreateJsonResponse(albumItem); + } + + /// + /// Creates a search hints response (Jellyfin search format). + /// + public IActionResult CreateSearchHintsResponse( + List songs, + List albums, + List artists) + { + var searchHints = new List>(); + + // Add artists first + foreach (var artist in artists) + { + searchHints.Add(new Dictionary + { + ["Id"] = artist.Id, + ["Name"] = artist.Name, + ["Type"] = "MusicArtist", + ["RunTimeTicks"] = 0, + ["PrimaryImageAspectRatio"] = 1.0, + ["ImageTags"] = new Dictionary + { + ["Primary"] = artist.Id + } + }); + } + + // Add albums + foreach (var album in albums) + { + searchHints.Add(new Dictionary + { + ["Id"] = album.Id, + ["Name"] = album.Title, + ["Type"] = "MusicAlbum", + ["Album"] = album.Title, + ["AlbumArtist"] = album.Artist, + ["ProductionYear"] = album.Year, + ["RunTimeTicks"] = 0, + ["ImageTags"] = new Dictionary + { + ["Primary"] = album.Id + } + }); + } + + // Add songs + foreach (var song in songs) + { + searchHints.Add(new Dictionary + { + ["Id"] = song.Id, + ["Name"] = song.Title, + ["Type"] = "Audio", + ["Album"] = song.Album, + ["AlbumArtist"] = song.Artist, + ["Artists"] = new[] { song.Artist }, + ["RunTimeTicks"] = (song.Duration ?? 0) * TimeSpan.TicksPerSecond, + ["ImageTags"] = new Dictionary + { + ["Primary"] = song.Id + } + }); + } + + return CreateJsonResponse(new + { + SearchHints = searchHints, + TotalRecordCount = searchHints.Count + }); + } + + /// + /// Creates an error response in Jellyfin format. + /// + public IActionResult CreateError(int statusCode, string message) + { + return new ObjectResult(new + { + type = "https://tools.ietf.org/html/rfc7231#section-6.5.4", + title = message, + status = statusCode + }) + { + StatusCode = statusCode + }; + } + + /// + /// Creates a JSON response. + /// + public IActionResult CreateJsonResponse(object data) + { + return new JsonResult(data); + } + + /// + /// Converts a Song domain model to a Jellyfin item. + /// + public Dictionary ConvertSongToJellyfinItem(Song song) + { + var item = new Dictionary + { + ["Id"] = song.Id, + ["Name"] = song.Title, + ["ServerId"] = "allstarr", + ["Type"] = "Audio", + ["MediaType"] = "Audio", + ["IsFolder"] = false, + ["Album"] = song.Album, + ["AlbumId"] = song.AlbumId ?? song.Id, + ["AlbumArtist"] = song.AlbumArtist ?? song.Artist, + ["Artists"] = new[] { song.Artist }, + ["ArtistItems"] = new[] + { + new Dictionary + { + ["Id"] = song.ArtistId ?? song.Id, + ["Name"] = song.Artist + } + }, + ["IndexNumber"] = song.Track, + ["ParentIndexNumber"] = song.DiscNumber ?? 1, + ["ProductionYear"] = song.Year, + ["RunTimeTicks"] = (song.Duration ?? 0) * TimeSpan.TicksPerSecond, + ["ImageTags"] = new Dictionary + { + ["Primary"] = song.Id + }, + ["BackdropImageTags"] = new string[0], + ["ImageBlurHashes"] = new Dictionary(), + ["LocationType"] = "FileSystem", // External content appears as local files to clients + ["Path"] = $"/music/{song.Artist}/{song.Album}/{song.Title}.flac", // Fake path for client compatibility + ["ChannelId"] = (object?)null, // Match Jellyfin structure + ["UserData"] = new Dictionary + { + ["PlaybackPositionTicks"] = 0, + ["PlayCount"] = 0, + ["IsFavorite"] = false, + ["Played"] = false, + ["Key"] = $"Audio-{song.Id}" + }, + ["CanDownload"] = true, + ["SupportsSync"] = true + }; + + // Add provider IDs for external content + if (!song.IsLocal && !string.IsNullOrEmpty(song.ExternalProvider)) + { + item["ProviderIds"] = new Dictionary + { + [song.ExternalProvider] = song.ExternalId ?? "" + }; + + if (!string.IsNullOrEmpty(song.Isrc)) + { + var providerIds = (Dictionary)item["ProviderIds"]!; + providerIds["ISRC"] = song.Isrc; + } + } + + if (!string.IsNullOrEmpty(song.Genre)) + { + item["Genres"] = new[] { song.Genre }; + } + + return item; + } + + /// + /// Converts an Album domain model to a Jellyfin item. + /// + public Dictionary ConvertAlbumToJellyfinItem(Album album) + { + var item = new Dictionary + { + ["Id"] = album.Id, + ["Name"] = album.Title, + ["ServerId"] = "allstarr", + ["Type"] = "MusicAlbum", + ["IsFolder"] = true, + ["AlbumArtist"] = album.Artist, + ["AlbumArtists"] = new[] + { + new Dictionary + { + ["Id"] = album.ArtistId ?? album.Id, + ["Name"] = album.Artist + } + }, + ["ProductionYear"] = album.Year, + ["ChildCount"] = album.SongCount ?? album.Songs.Count, + ["ImageTags"] = new Dictionary + { + ["Primary"] = album.Id + }, + ["BackdropImageTags"] = new string[0], + ["ImageBlurHashes"] = new Dictionary(), + ["LocationType"] = "FileSystem", // External content appears as local files to clients + ["MediaType"] = (object?)null, // Match Jellyfin structure + ["ChannelId"] = (object?)null, // Match Jellyfin structure + ["CollectionType"] = (object?)null, // Match Jellyfin structure + ["UserData"] = new Dictionary + { + ["PlaybackPositionTicks"] = 0, + ["PlayCount"] = 0, + ["IsFavorite"] = false, + ["Played"] = false, + ["Key"] = album.Id + } + }; + + // Add provider IDs for external content + if (!album.IsLocal && !string.IsNullOrEmpty(album.ExternalProvider)) + { + item["ProviderIds"] = new Dictionary + { + [album.ExternalProvider] = album.ExternalId ?? "" + }; + } + + if (!string.IsNullOrEmpty(album.Genre)) + { + item["Genres"] = new[] { album.Genre }; + } + + return item; + } + + /// + /// Converts an Artist domain model to a Jellyfin item. + /// + public Dictionary ConvertArtistToJellyfinItem(Artist artist) + { + var item = new Dictionary + { + ["Id"] = artist.Id, + ["Name"] = artist.Name, + ["ServerId"] = "allstarr", + ["Type"] = "MusicArtist", + ["IsFolder"] = true, + ["AlbumCount"] = artist.AlbumCount ?? 0, + ["ImageTags"] = new Dictionary + { + ["Primary"] = artist.Id + }, + ["BackdropImageTags"] = new string[0], + ["ImageBlurHashes"] = new Dictionary(), + ["LocationType"] = "FileSystem", // External content appears as local files to clients + ["MediaType"] = (object?)null, // Match Jellyfin structure + ["ChannelId"] = (object?)null, // Match Jellyfin structure + ["CollectionType"] = (object?)null, // Match Jellyfin structure + ["UserData"] = new Dictionary + { + ["PlaybackPositionTicks"] = 0, + ["PlayCount"] = 0, + ["IsFavorite"] = false, + ["Played"] = false, + ["Key"] = artist.Id + } + }; + + // Add provider IDs for external content + if (!artist.IsLocal && !string.IsNullOrEmpty(artist.ExternalProvider)) + { + item["ProviderIds"] = new Dictionary + { + [artist.ExternalProvider] = artist.ExternalId ?? "" + }; + } + + return item; + } + + /// + /// Converts a Jellyfin JSON element to a dictionary. + /// + public object ConvertJellyfinJsonElement(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.Object => element.EnumerateObject() + .ToDictionary(p => p.Name, p => ConvertJellyfinJsonElement(p.Value)), + JsonValueKind.Array => element.EnumerateArray() + .Select(ConvertJellyfinJsonElement) + .ToList(), + JsonValueKind.String => element.GetString() ?? "", + JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null!, + _ => element.ToString() + }; + } + + /// + /// Converts an ExternalPlaylist to a Jellyfin album item. + /// + public Dictionary ConvertPlaylistToJellyfinItem(ExternalPlaylist playlist) + { + var curatorName = !string.IsNullOrEmpty(playlist.CuratorName) + ? playlist.CuratorName + : playlist.Provider; + + var item = new Dictionary + { + ["Id"] = playlist.Id, + ["Name"] = playlist.Name, + ["ServerId"] = "allstarr", + ["Type"] = "Playlist", + ["IsFolder"] = true, + ["AlbumArtist"] = curatorName, + ["Genres"] = new[] { "Playlist" }, + ["ChildCount"] = playlist.TrackCount, + ["ImageTags"] = new Dictionary + { + ["Primary"] = playlist.Id + }, + ["BackdropImageTags"] = new string[0], + ["ImageBlurHashes"] = new Dictionary(), + ["LocationType"] = "FileSystem", + ["MediaType"] = (object?)null, + ["ChannelId"] = (object?)null, + ["CollectionType"] = (object?)null, + ["ProviderIds"] = new Dictionary + { + [playlist.Provider] = playlist.ExternalId + }, + ["UserData"] = new Dictionary + { + ["PlaybackPositionTicks"] = 0, + ["PlayCount"] = 0, + ["IsFavorite"] = false, + ["Played"] = false, + ["Key"] = playlist.Id + } + }; + + if (playlist.CreatedDate.HasValue) + { + item["PremiereDate"] = playlist.CreatedDate.Value.ToString("o"); + item["ProductionYear"] = playlist.CreatedDate.Value.Year; + } + + return item; + } + public Dictionary ConvertPlaylistToAlbumItem(ExternalPlaylist playlist) + { + var curatorName = !string.IsNullOrEmpty(playlist.CuratorName) + ? playlist.CuratorName + : playlist.Provider; + + var item = new Dictionary + { + ["Id"] = playlist.Id, + ["Name"] = playlist.Name, + ["Type"] = "Playlist", + ["IsFolder"] = true, + ["AlbumArtist"] = curatorName, + ["ChildCount"] = playlist.TrackCount, + ["RunTimeTicks"] = playlist.Duration * TimeSpan.TicksPerSecond, + ["Genres"] = new[] { "Playlist" }, + ["ImageTags"] = new Dictionary + { + ["Primary"] = playlist.Id + }, + ["ProviderIds"] = new Dictionary + { + [playlist.Provider] = playlist.ExternalId + } + }; + + if (playlist.CreatedDate.HasValue) + { + item["PremiereDate"] = playlist.CreatedDate.Value.ToString("o"); + item["ProductionYear"] = playlist.CreatedDate.Value.Year; + } + + return item; + } +} diff --git a/allstarr/Services/Jellyfin/JellyfinStartupValidator.cs b/allstarr/Services/Jellyfin/JellyfinStartupValidator.cs new file mode 100644 index 0000000..c4977e8 --- /dev/null +++ b/allstarr/Services/Jellyfin/JellyfinStartupValidator.cs @@ -0,0 +1,214 @@ +using Microsoft.Extensions.Options; +using allstarr.Models.Settings; +using allstarr.Services.Validation; + +namespace allstarr.Services.Jellyfin; + +/// +/// Validates Jellyfin server connectivity at startup. +/// +public class JellyfinStartupValidator : BaseStartupValidator +{ + private readonly IOptions _settings; + + public override string ServiceName => "Jellyfin"; + + public JellyfinStartupValidator(IOptions settings, HttpClient httpClient) + : base(httpClient) + { + _settings = settings; + } + + public override async Task ValidateAsync(CancellationToken cancellationToken) + { + var settings = _settings.Value; + + if (string.IsNullOrWhiteSpace(settings.Url)) + { + WriteStatus("Jellyfin URL", "NOT CONFIGURED", ConsoleColor.Red); + WriteDetail("Set the Jellyfin__Url environment variable"); + return ValidationResult.NotConfigured("Jellyfin URL not configured"); + } + + WriteStatus("Jellyfin URL", settings.Url, ConsoleColor.Cyan); + + // API Key is optional - only needed for server-to-server operations + // Client authentication uses username/password via /Users/AuthenticateByName + if (!string.IsNullOrWhiteSpace(settings.ApiKey)) + { + WriteStatus("API Key", MaskSecret(settings.ApiKey), ConsoleColor.DarkGray); + WriteDetail("(Optional - for server operations)"); + } + + if (!string.IsNullOrWhiteSpace(settings.UserId)) + { + WriteStatus("User ID", MaskSecret(settings.UserId), ConsoleColor.DarkGray); + WriteDetail("(Optional - for server operations)"); + } + + try + { + // Test connection using public system info endpoint (no auth required) + var publicInfoUrl = $"{settings.Url.TrimEnd('/')}/System/Info/Public"; + var response = await _httpClient.GetAsync(publicInfoUrl, cancellationToken); + + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(cancellationToken); + + // Try to parse server info + string? serverName = null; + string? version = null; + + if (content.Contains("ServerName")) + { + var nameStart = content.IndexOf("\"ServerName\":", StringComparison.Ordinal); + if (nameStart >= 0) + { + nameStart = content.IndexOf('"', nameStart + 13) + 1; + var nameEnd = content.IndexOf('"', nameStart); + if (nameEnd > nameStart) + { + serverName = content[nameStart..nameEnd]; + } + } + } + + if (content.Contains("Version")) + { + var verStart = content.IndexOf("\"Version\":", StringComparison.Ordinal); + if (verStart >= 0) + { + verStart = content.IndexOf('"', verStart + 10) + 1; + var verEnd = content.IndexOf('"', verStart); + if (verEnd > verStart) + { + version = content[verStart..verEnd]; + } + } + } + + var serverInfo = !string.IsNullOrEmpty(serverName) + ? $"{serverName} (v{version ?? "unknown"})" + : "OK"; + + WriteStatus("Jellyfin server", serverInfo, ConsoleColor.Green); + + // Test authenticated access if API key is configured + if (!string.IsNullOrWhiteSpace(settings.ApiKey)) + { + await ValidateAuthenticatedAccessAsync(settings, cancellationToken); + } + + return ValidationResult.Success($"Connected to {serverInfo}"); + } + else + { + WriteStatus("Jellyfin server", $"HTTP {(int)response.StatusCode}", ConsoleColor.Red); + return ValidationResult.Failure($"HTTP {(int)response.StatusCode}", + "Jellyfin server returned an error", ConsoleColor.Red); + } + } + catch (TaskCanceledException) + { + WriteStatus("Jellyfin server", "TIMEOUT", ConsoleColor.Red); + WriteDetail("Could not reach server within 10 seconds"); + return ValidationResult.Failure("TIMEOUT", "Could not reach server within timeout period", ConsoleColor.Red); + } + catch (HttpRequestException ex) + { + WriteStatus("Jellyfin server", "UNREACHABLE", ConsoleColor.Red); + WriteDetail(ex.Message); + return ValidationResult.Failure("UNREACHABLE", ex.Message, ConsoleColor.Red); + } + catch (Exception ex) + { + WriteStatus("Jellyfin server", "ERROR", ConsoleColor.Red); + WriteDetail(ex.Message); + return ValidationResult.Failure("ERROR", ex.Message, ConsoleColor.Red); + } + } + + private async Task ValidateAuthenticatedAccessAsync(JellyfinSettings settings, CancellationToken cancellationToken) + { + try + { + var authHeader = $"MediaBrowser Client=\"{settings.ClientName}\", " + + $"Device=\"{settings.DeviceName}\", " + + $"DeviceId=\"{settings.DeviceId}\", " + + $"Version=\"{settings.ClientVersion}\", " + + $"Token=\"{settings.ApiKey}\""; + + using var request = new HttpRequestMessage(HttpMethod.Get, + $"{settings.Url.TrimEnd('/')}/System/Info"); + request.Headers.Add("Authorization", authHeader); + + var response = await _httpClient.SendAsync(request, cancellationToken); + + if (response.IsSuccessStatusCode) + { + WriteStatus("Authentication", "OK", ConsoleColor.Green); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + WriteStatus("Authentication", "INVALID API KEY", ConsoleColor.Red); + WriteDetail("Check your Jellyfin API key configuration"); + } + else + { + WriteStatus("Authentication", $"HTTP {(int)response.StatusCode}", ConsoleColor.Yellow); + } + + // Check if we can access the music library + if (!string.IsNullOrWhiteSpace(settings.LibraryId)) + { + WriteStatus("Library ID", settings.LibraryId, ConsoleColor.DarkGray); + } + else + { + await TryDetectMusicLibraryAsync(settings, authHeader, cancellationToken); + } + } + catch (Exception ex) + { + WriteStatus("Authentication", "ERROR", ConsoleColor.Yellow); + WriteDetail(ex.Message); + } + } + + private async Task TryDetectMusicLibraryAsync(JellyfinSettings settings, string authHeader, CancellationToken cancellationToken) + { + try + { + var url = $"{settings.Url.TrimEnd('/')}/Library/MediaFolders"; + if (!string.IsNullOrWhiteSpace(settings.UserId)) + { + url += $"?userId={settings.UserId}"; + } + + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Authorization", authHeader); + + var response = await _httpClient.SendAsync(request, cancellationToken); + + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(cancellationToken); + + if (content.Contains("\"CollectionType\":\"music\"")) + { + WriteStatus("Music library", "DETECTED", ConsoleColor.Green); + } + else + { + WriteStatus("Music library", "NOT FOUND", ConsoleColor.Yellow); + WriteDetail("No music library detected. Set Jellyfin__LibraryId to specify one."); + } + } + } + catch + { + // Silently ignore - not critical for startup + } + } +} diff --git a/octo-fiesta/Services/Local/ILocalLibraryService.cs b/allstarr/Services/Local/ILocalLibraryService.cs similarity index 87% rename from octo-fiesta/Services/Local/ILocalLibraryService.cs rename to allstarr/Services/Local/ILocalLibraryService.cs index ce45d81..2f4818f 100644 --- a/octo-fiesta/Services/Local/ILocalLibraryService.cs +++ b/allstarr/Services/Local/ILocalLibraryService.cs @@ -1,10 +1,10 @@ -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; -namespace octo_fiesta.Services.Local; +namespace allstarr.Services.Local; /// /// Interface for local music library management diff --git a/octo-fiesta/Services/Local/LocalLibraryService.cs b/allstarr/Services/Local/LocalLibraryService.cs similarity index 95% rename from octo-fiesta/Services/Local/LocalLibraryService.cs rename to allstarr/Services/Local/LocalLibraryService.cs index ac6d57d..527a368 100644 --- a/octo-fiesta/Services/Local/LocalLibraryService.cs +++ b/allstarr/Services/Local/LocalLibraryService.cs @@ -1,13 +1,13 @@ using System.Text.Json; using Microsoft.Extensions.Options; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; -using octo_fiesta.Services; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; +using allstarr.Services; -namespace octo_fiesta.Services.Local; +namespace allstarr.Services.Local; /// /// Local library service implementation diff --git a/octo-fiesta/Services/Qobuz/QobuzBundleService.cs b/allstarr/Services/Qobuz/QobuzBundleService.cs similarity index 99% rename from octo-fiesta/Services/Qobuz/QobuzBundleService.cs rename to allstarr/Services/Qobuz/QobuzBundleService.cs index d5385ac..a15d60b 100644 --- a/octo-fiesta/Services/Qobuz/QobuzBundleService.cs +++ b/allstarr/Services/Qobuz/QobuzBundleService.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace octo_fiesta.Services.Qobuz; +namespace allstarr.Services.Qobuz; /// /// Service to dynamically extract Qobuz App ID and secrets from the Qobuz web player diff --git a/octo-fiesta/Services/Qobuz/QobuzDownloadService.cs b/allstarr/Services/Qobuz/QobuzDownloadService.cs similarity index 97% rename from octo-fiesta/Services/Qobuz/QobuzDownloadService.cs rename to allstarr/Services/Qobuz/QobuzDownloadService.cs index 483f5d6..5da212f 100644 --- a/octo-fiesta/Services/Qobuz/QobuzDownloadService.cs +++ b/allstarr/Services/Qobuz/QobuzDownloadService.cs @@ -1,18 +1,18 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; -using octo_fiesta.Services.Local; -using octo_fiesta.Services.Common; -using octo_fiesta.Services.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; +using allstarr.Services.Local; +using allstarr.Services.Common; +using allstarr.Services.Subsonic; using Microsoft.Extensions.Options; using IOFile = System.IO.File; -namespace octo_fiesta.Services.Qobuz; +namespace allstarr.Services.Qobuz; /// /// Download service implementation for Qobuz diff --git a/octo-fiesta/Services/Qobuz/QobuzMetadataService.cs b/allstarr/Services/Qobuz/QobuzMetadataService.cs similarity index 99% rename from octo-fiesta/Services/Qobuz/QobuzMetadataService.cs rename to allstarr/Services/Qobuz/QobuzMetadataService.cs index df97d11..c718fe7 100644 --- a/octo-fiesta/Services/Qobuz/QobuzMetadataService.cs +++ b/allstarr/Services/Qobuz/QobuzMetadataService.cs @@ -1,12 +1,12 @@ -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; using System.Text.Json; using Microsoft.Extensions.Options; -namespace octo_fiesta.Services.Qobuz; +namespace allstarr.Services.Qobuz; /// /// Metadata service implementation using the Qobuz API diff --git a/octo-fiesta/Services/Qobuz/QobuzStartupValidator.cs b/allstarr/Services/Qobuz/QobuzStartupValidator.cs similarity index 97% rename from octo-fiesta/Services/Qobuz/QobuzStartupValidator.cs rename to allstarr/Services/Qobuz/QobuzStartupValidator.cs index 6f8eb7f..1cccf7a 100644 --- a/octo-fiesta/Services/Qobuz/QobuzStartupValidator.cs +++ b/allstarr/Services/Qobuz/QobuzStartupValidator.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.Options; -using octo_fiesta.Models.Settings; -using octo_fiesta.Services.Validation; +using allstarr.Models.Settings; +using allstarr.Services.Validation; -namespace octo_fiesta.Services.Qobuz; +namespace allstarr.Services.Qobuz; /// /// Validates Qobuz credentials at startup diff --git a/octo-fiesta/Services/SquidWTF/SquidWTFDownloadService.cs b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs similarity index 96% rename from octo-fiesta/Services/SquidWTF/SquidWTFDownloadService.cs rename to allstarr/Services/SquidWTF/SquidWTFDownloadService.cs index 6d0a222..ec30a86 100644 --- a/octo-fiesta/Services/SquidWTF/SquidWTFDownloadService.cs +++ b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs @@ -1,17 +1,17 @@ using System.Text; using System.Text.Json; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; -using octo_fiesta.Services.Local; -using octo_fiesta.Services.Common; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; +using allstarr.Services.Local; +using allstarr.Services.Common; using Microsoft.Extensions.Options; using IOFile = System.IO.File; using Microsoft.Extensions.Logging; -namespace octo_fiesta.Services.SquidWTF; +namespace allstarr.Services.SquidWTF; /// /// Handles track downloading from tidal.squid.wtf (no encryption, no auth required) diff --git a/octo-fiesta/Services/SquidWTF/SquidWTFMetadataService.cs b/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs similarity index 82% rename from octo-fiesta/Services/SquidWTF/SquidWTFMetadataService.cs rename to allstarr/Services/SquidWTF/SquidWTFMetadataService.cs index 9c661f1..506e5a7 100644 --- a/octo-fiesta/Services/SquidWTF/SquidWTFMetadataService.cs +++ b/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs @@ -1,15 +1,15 @@ -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Download; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Download; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; +using allstarr.Services.Common; using System.Text.Json; using System.Text; using Microsoft.Extensions.Options; using System.Text.Json.Nodes; -//using Microsoft.Extensions.Logging; -namespace octo_fiesta.Services.SquidWTF; +namespace allstarr.Services.SquidWTF; /// /// Metadata service implementation using the SquidWTF API (free, no key required) @@ -17,24 +17,27 @@ namespace octo_fiesta.Services.SquidWTF; public class SquidWTFMetadataService : IMusicMetadataService { - private readonly HttpClient _httpClient; + private readonly HttpClient _httpClient; private readonly SubsonicSettings _settings; - private readonly ILogger _logger; + private readonly ILogger _logger; + private readonly RedisCacheService _cache; private const string BaseUrl = "https://triton.squid.wtf"; - public SquidWTFMetadataService(IHttpClientFactory httpClientFactory, - IOptions settings, - IOptions squidwtfSettings, - ILogger logger) + public SquidWTFMetadataService( + IHttpClientFactory httpClientFactory, + IOptions settings, + IOptions squidwtfSettings, + ILogger logger, + RedisCacheService cache) { _httpClient = httpClientFactory.CreateClient(); _settings = settings.Value; - _logger = logger; - - // Set up default headers + _logger = logger; + _cache = cache; + + // Set up default headers _httpClient.DefaultRequestHeaders.Add("User-Agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"); - + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"); } public async Task> SearchSongsAsync(string query, int limit = 20) @@ -43,33 +46,37 @@ public class SquidWTFMetadataService : IMusicMetadataService { var url = $"{BaseUrl}/search/?s={Uri.EscapeDataString(query)}"; var response = await _httpClient.GetAsync(url); - - if (!response.IsSuccessStatusCode) return new List(); + + if (!response.IsSuccessStatusCode) + { + return new List(); + } var json = await response.Content.ReadAsStringAsync(); var result = JsonDocument.Parse(json); var songs = new List(); if (result.RootElement.TryGetProperty("data", out var data) && - data.TryGetProperty("items", out var items)) + data.TryGetProperty("items", out var items)) { - int count = 0; + int count = 0; foreach (var track in items.EnumerateArray()) { - if (count >= limit) break; + if (count >= limit) break; + var song = ParseTidalTrack(track); songs.Add(song); - count++; - + count++; } } return songs; } catch (Exception ex) - { - return new List(); + { + _logger.LogWarning(ex, "Failed to search songs for query: {Query}", query); + return new List(); } - } + } public async Task> SearchAlbumsAsync(string query, int limit = 20) { @@ -78,29 +85,34 @@ public class SquidWTFMetadataService : IMusicMetadataService var url = $"{BaseUrl}/search/?al={Uri.EscapeDataString(query)}"; var response = await _httpClient.GetAsync(url); - if (!response.IsSuccessStatusCode) return new List(); + if (!response.IsSuccessStatusCode) + { + return new List(); + } var json = await response.Content.ReadAsStringAsync(); var result = JsonDocument.Parse(json); var albums = new List(); if (result.RootElement.TryGetProperty("data", out var data) && - data.TryGetProperty("albums", out var albumsObj) && - albumsObj.TryGetProperty("items", out var items)) + data.TryGetProperty("albums", out var albumsObj) && + albumsObj.TryGetProperty("items", out var items)) { - int count = 0; + int count = 0; foreach (var album in items.EnumerateArray()) { - if (count >= limit) break; + if (count >= limit) break; + albums.Add(ParseTidalAlbum(album)); - count++; + count++; } } - + return albums; } - catch + catch (Exception ex) { + _logger.LogWarning(ex, "Failed to search albums for query: {Query}", query); return new List(); } } @@ -111,29 +123,35 @@ public class SquidWTFMetadataService : IMusicMetadataService { var url = $"{BaseUrl}/search/?a={Uri.EscapeDataString(query)}"; var response = await _httpClient.GetAsync(url); - if (!response.IsSuccessStatusCode) return new List(); + + if (!response.IsSuccessStatusCode) + { + return new List(); + } var json = await response.Content.ReadAsStringAsync(); var result = JsonDocument.Parse(json); var artists = new List(); if (result.RootElement.TryGetProperty("data", out var data) && - data.TryGetProperty("artists", out var artistsObj) && - artistsObj.TryGetProperty("items", out var items)) + data.TryGetProperty("artists", out var artistsObj) && + artistsObj.TryGetProperty("items", out var items)) { - int count = 0; + int count = 0; foreach (var artist in items.EnumerateArray()) { - if (count >= limit) break; + if (count >= limit) break; + artists.Add(ParseTidalArtist(artist)); - count++; + count++; } } return artists; } - catch + catch (Exception ex) { + _logger.LogWarning(ex, "Failed to search artists for query: {Query}", query); return new List(); } } @@ -219,6 +237,11 @@ public class SquidWTFMetadataService : IMusicMetadataService { if (externalProvider != "squidwtf") return null; + // Try cache first + var cacheKey = $"squidwtf:album:{externalId}"; + var cached = await _cache.GetAsync(cacheKey); + if (cached != null) return cached; + try { // Use the /info endpoint for full track metadata @@ -251,6 +274,10 @@ public class SquidWTFMetadataService : IMusicMetadataService } } } + + // Cache for 24 hours + await _cache.SetAsync(cacheKey, album, TimeSpan.FromHours(24)); + return album; } catch (Exception ex) @@ -263,16 +290,33 @@ public class SquidWTFMetadataService : IMusicMetadataService public async Task GetArtistAsync(string externalProvider, string externalId) { if (externalProvider != "squidwtf") return null; + + _logger.LogInformation("GetArtistAsync called for SquidWTF artist {ExternalId}", externalId); + + // Try cache first + var cacheKey = $"squidwtf:artist:{externalId}"; + var cached = await _cache.GetAsync(cacheKey); + if (cached != null) + { + _logger.LogInformation("Returning cached artist {ArtistName}", cached.Name); + return cached; + } try { // Use the /info endpoint for full track metadata var url = $"{BaseUrl}/artist/?f={externalId}"; + _logger.LogInformation("Fetching artist from {Url}", url); var response = await _httpClient.GetAsync(url); - if (!response.IsSuccessStatusCode) return null; + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("SquidWTF artist request failed with status {StatusCode}", response.StatusCode); + return null; + } var json = await response.Content.ReadAsStringAsync(); + _logger.LogDebug("SquidWTF artist response: {Json}", json.Length > 500 ? json.Substring(0, 500) + "..." : json); var result = JsonDocument.Parse(json); JsonElement? artistSource = null; @@ -285,6 +329,7 @@ public class SquidWTFMetadataService : IMusicMetadataService { albumCount = albumItems.GetArrayLength(); artistSource = albumItems[0].GetProperty("artist"); + _logger.LogInformation("Found artist from albums, albumCount={AlbumCount}", albumCount); } // Think this can maybe switch to something using ParseTidalTrack @@ -294,9 +339,14 @@ public class SquidWTFMetadataService : IMusicMetadataService artists.GetArrayLength() > 0) { artistSource = artists[0]; + _logger.LogInformation("Found artist from tracks"); } - if (artistSource == null) return null; + if (artistSource == null) + { + _logger.LogWarning("Could not find artist data in response"); + return null; + } var artistElement = artistSource.Value; var normalizedArtist = new JsonObject @@ -308,7 +358,14 @@ public class SquidWTFMetadataService : IMusicMetadataService }; using var doc = JsonDocument.Parse(normalizedArtist.ToJsonString()); - return ParseTidalArtist(doc.RootElement); + var artist = ParseTidalArtist(doc.RootElement); + + _logger.LogInformation("Successfully parsed artist {ArtistName} with {AlbumCount} albums", artist.Name, albumCount); + + // Cache for 24 hours + await _cache.SetAsync(cacheKey, artist, TimeSpan.FromHours(24)); + + return artist; } catch (Exception ex) @@ -325,12 +382,20 @@ public class SquidWTFMetadataService : IMusicMetadataService { if (externalProvider != "squidwtf") return new List(); + _logger.LogInformation("GetArtistAlbumsAsync called for SquidWTF artist {ExternalId}", externalId); + var url = $"{BaseUrl}/artist/?f={externalId}"; + _logger.LogInformation("Fetching artist albums from URL: {Url}", url); var response = await _httpClient.GetAsync(url); - if (!response.IsSuccessStatusCode) return new List(); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("SquidWTF artist albums request failed with status {StatusCode}", response.StatusCode); + return new List(); + } var json = await response.Content.ReadAsStringAsync(); + _logger.LogDebug("SquidWTF artist albums response for {ExternalId}: {JsonLength} bytes", externalId, json.Length); var result = JsonDocument.Parse(json); var albums = new List(); @@ -340,15 +405,23 @@ public class SquidWTFMetadataService : IMusicMetadataService { foreach (var album in items.EnumerateArray()) { - albums.Add(ParseTidalAlbum(album)); + var parsedAlbum = ParseTidalAlbum(album); + _logger.LogInformation("Parsed album: {AlbumTitle} by {ArtistName} (ArtistId: {ArtistId})", + parsedAlbum.Title, parsedAlbum.Artist, parsedAlbum.ArtistId); + albums.Add(parsedAlbum); } + _logger.LogInformation("Found {AlbumCount} albums for artist {ExternalId}", albums.Count, externalId); } + else + { + _logger.LogWarning("No albums found in response for artist {ExternalId}", externalId); + } return albums; } catch (Exception ex) { - _logger.LogError(ex, "Failed to get SquidWTF artist albums for {ExternalId}"); + _logger.LogError(ex, "Failed to get SquidWTF artist albums for {ExternalId}", externalId); return new List(); } } diff --git a/octo-fiesta/Services/SquidWTF/SquidWTFStartupValidator.cs b/allstarr/Services/SquidWTF/SquidWTFStartupValidator.cs similarity index 97% rename from octo-fiesta/Services/SquidWTF/SquidWTFStartupValidator.cs rename to allstarr/Services/SquidWTF/SquidWTFStartupValidator.cs index 7c52d09..418859a 100644 --- a/octo-fiesta/Services/SquidWTF/SquidWTFStartupValidator.cs +++ b/allstarr/Services/SquidWTF/SquidWTFStartupValidator.cs @@ -1,10 +1,10 @@ using System.Text; using System.Text.Json; using Microsoft.Extensions.Options; -using octo_fiesta.Models.Settings; -using octo_fiesta.Services.Validation; +using allstarr.Models.Settings; +using allstarr.Services.Validation; -namespace octo_fiesta.Services.SquidWTF; +namespace allstarr.Services.SquidWTF; /// /// Validates SquidWTF service connectivity at startup (no auth needed) diff --git a/octo-fiesta/Services/StartupValidationService.cs b/allstarr/Services/StartupValidationService.cs similarity index 95% rename from octo-fiesta/Services/StartupValidationService.cs rename to allstarr/Services/StartupValidationService.cs index c163603..8a522cf 100644 --- a/octo-fiesta/Services/StartupValidationService.cs +++ b/allstarr/Services/StartupValidationService.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Options; -using octo_fiesta.Models.Settings; -using octo_fiesta.Services.Deezer; -using octo_fiesta.Services.Qobuz; +using allstarr.Models.Settings; +using allstarr.Services.Deezer; +using allstarr.Services.Qobuz; -namespace octo_fiesta.Services; +namespace allstarr.Services; /// /// Hosted service that validates configuration at startup and logs the results. @@ -36,7 +36,7 @@ public class StartupValidationService : IHostedService { Console.WriteLine(); Console.WriteLine("========================================"); - Console.WriteLine(" octo-fiesta starting up... "); + Console.WriteLine(" allstarr starting up... "); Console.WriteLine("========================================"); Console.WriteLine(); @@ -82,7 +82,7 @@ public class StartupValidationService : IHostedService try { - var pingUrl = $"{subsonicUrl.TrimEnd('/')}/rest/ping.view?v=1.16.1&c=octo-fiesta&f=json"; + var pingUrl = $"{subsonicUrl.TrimEnd('/')}/rest/ping.view?v=1.16.1&c=allstarr&f=json"; var response = await _httpClient.GetAsync(pingUrl, cancellationToken); if (response.IsSuccessStatusCode) diff --git a/octo-fiesta/Services/Subsonic/PlaylistSyncService.cs b/allstarr/Services/Subsonic/PlaylistSyncService.cs similarity index 98% rename from octo-fiesta/Services/Subsonic/PlaylistSyncService.cs rename to allstarr/Services/Subsonic/PlaylistSyncService.cs index c3818f4..0903213 100644 --- a/octo-fiesta/Services/Subsonic/PlaylistSyncService.cs +++ b/allstarr/Services/Subsonic/PlaylistSyncService.cs @@ -1,13 +1,13 @@ using System.Collections.Concurrent; using System.Text; using Microsoft.Extensions.Options; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Settings; -using octo_fiesta.Models.Subsonic; -using octo_fiesta.Services.Common; +using allstarr.Models.Domain; +using allstarr.Models.Settings; +using allstarr.Models.Subsonic; +using allstarr.Services.Common; using IOFile = System.IO.File; -namespace octo_fiesta.Services.Subsonic; +namespace allstarr.Services.Subsonic; /// /// Service responsible for downloading playlist tracks and creating M3U files diff --git a/octo-fiesta/Services/Subsonic/SubsonicModelMapper.cs b/allstarr/Services/Subsonic/SubsonicModelMapper.cs similarity index 99% rename from octo-fiesta/Services/Subsonic/SubsonicModelMapper.cs rename to allstarr/Services/Subsonic/SubsonicModelMapper.cs index 85cbe35..09542bc 100644 --- a/octo-fiesta/Services/Subsonic/SubsonicModelMapper.cs +++ b/allstarr/Services/Subsonic/SubsonicModelMapper.cs @@ -1,10 +1,10 @@ using System.Text; using System.Text.Json; using System.Xml.Linq; -using octo_fiesta.Models.Search; -using octo_fiesta.Models.Subsonic; +using allstarr.Models.Search; +using allstarr.Models.Subsonic; -namespace octo_fiesta.Services.Subsonic; +namespace allstarr.Services.Subsonic; /// /// Handles parsing Subsonic API responses and merging local with external search results. diff --git a/octo-fiesta/Services/Subsonic/SubsonicProxyService.cs b/allstarr/Services/Subsonic/SubsonicProxyService.cs similarity index 98% rename from octo-fiesta/Services/Subsonic/SubsonicProxyService.cs rename to allstarr/Services/Subsonic/SubsonicProxyService.cs index 265575f..34a9a31 100644 --- a/octo-fiesta/Services/Subsonic/SubsonicProxyService.cs +++ b/allstarr/Services/Subsonic/SubsonicProxyService.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; -using octo_fiesta.Models.Settings; +using allstarr.Models.Settings; -namespace octo_fiesta.Services.Subsonic; +namespace allstarr.Services.Subsonic; /// /// Handles proxying requests to the underlying Subsonic server. diff --git a/octo-fiesta/Services/Subsonic/SubsonicRequestParser.cs b/allstarr/Services/Subsonic/SubsonicRequestParser.cs similarity index 98% rename from octo-fiesta/Services/Subsonic/SubsonicRequestParser.cs rename to allstarr/Services/Subsonic/SubsonicRequestParser.cs index 9aba076..a55d8b1 100644 --- a/octo-fiesta/Services/Subsonic/SubsonicRequestParser.cs +++ b/allstarr/Services/Subsonic/SubsonicRequestParser.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.WebUtilities; using System.Text.Json; -namespace octo_fiesta.Services.Subsonic; +namespace allstarr.Services.Subsonic; /// /// Service responsible for parsing HTTP request parameters from various sources diff --git a/octo-fiesta/Services/Subsonic/SubsonicResponseBuilder.cs b/allstarr/Services/Subsonic/SubsonicResponseBuilder.cs similarity index 99% rename from octo-fiesta/Services/Subsonic/SubsonicResponseBuilder.cs rename to allstarr/Services/Subsonic/SubsonicResponseBuilder.cs index b2fd810..26cadcc 100644 --- a/octo-fiesta/Services/Subsonic/SubsonicResponseBuilder.cs +++ b/allstarr/Services/Subsonic/SubsonicResponseBuilder.cs @@ -1,10 +1,10 @@ using Microsoft.AspNetCore.Mvc; using System.Xml.Linq; using System.Text.Json; -using octo_fiesta.Models.Domain; -using octo_fiesta.Models.Subsonic; +using allstarr.Models.Domain; +using allstarr.Models.Subsonic; -namespace octo_fiesta.Services.Subsonic; +namespace allstarr.Services.Subsonic; /// /// Handles building Subsonic API responses in both XML and JSON formats. diff --git a/octo-fiesta/Services/Validation/BaseStartupValidator.cs b/allstarr/Services/Validation/BaseStartupValidator.cs similarity index 98% rename from octo-fiesta/Services/Validation/BaseStartupValidator.cs rename to allstarr/Services/Validation/BaseStartupValidator.cs index 6f6d0bf..a0bcbd8 100644 --- a/octo-fiesta/Services/Validation/BaseStartupValidator.cs +++ b/allstarr/Services/Validation/BaseStartupValidator.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Services.Validation; +namespace allstarr.Services.Validation; /// /// Base class for startup validators providing common functionality diff --git a/octo-fiesta/Services/Validation/IStartupValidator.cs b/allstarr/Services/Validation/IStartupValidator.cs similarity index 92% rename from octo-fiesta/Services/Validation/IStartupValidator.cs rename to allstarr/Services/Validation/IStartupValidator.cs index 59e2fa6..c23464c 100644 --- a/octo-fiesta/Services/Validation/IStartupValidator.cs +++ b/allstarr/Services/Validation/IStartupValidator.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Services.Validation; +namespace allstarr.Services.Validation; /// /// Interface for service startup validators diff --git a/octo-fiesta/Services/Validation/StartupValidationOrchestrator.cs b/allstarr/Services/Validation/StartupValidationOrchestrator.cs similarity index 91% rename from octo-fiesta/Services/Validation/StartupValidationOrchestrator.cs rename to allstarr/Services/Validation/StartupValidationOrchestrator.cs index 672b8be..dca9420 100644 --- a/octo-fiesta/Services/Validation/StartupValidationOrchestrator.cs +++ b/allstarr/Services/Validation/StartupValidationOrchestrator.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Options; -using octo_fiesta.Models.Settings; +using allstarr.Models.Settings; -namespace octo_fiesta.Services.Validation; +namespace allstarr.Services.Validation; /// /// Orchestrates startup validation for all configured services. @@ -24,7 +24,7 @@ public class StartupValidationOrchestrator : IHostedService { Console.WriteLine(); Console.WriteLine("========================================"); - Console.WriteLine(" octo-fiesta starting up... "); + Console.WriteLine(" allstarr starting up... "); Console.WriteLine("========================================"); Console.WriteLine(); diff --git a/octo-fiesta/Services/Validation/SubsonicStartupValidator.cs b/allstarr/Services/Validation/SubsonicStartupValidator.cs similarity index 96% rename from octo-fiesta/Services/Validation/SubsonicStartupValidator.cs rename to allstarr/Services/Validation/SubsonicStartupValidator.cs index 926613a..6691fe5 100644 --- a/octo-fiesta/Services/Validation/SubsonicStartupValidator.cs +++ b/allstarr/Services/Validation/SubsonicStartupValidator.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Options; -using octo_fiesta.Models.Settings; +using allstarr.Models.Settings; -namespace octo_fiesta.Services.Validation; +namespace allstarr.Services.Validation; /// /// Validates Subsonic server connectivity at startup @@ -33,7 +33,7 @@ public class SubsonicStartupValidator : BaseStartupValidator try { - var pingUrl = $"{subsonicUrl.TrimEnd('/')}/rest/ping.view?v=1.16.1&c=octo-fiesta&f=json"; + var pingUrl = $"{subsonicUrl.TrimEnd('/')}/rest/ping.view?v=1.16.1&c=allstarr&f=json"; var response = await _httpClient.GetAsync(pingUrl, cancellationToken); if (response.IsSuccessStatusCode) diff --git a/octo-fiesta/Services/Validation/ValidationResult.cs b/allstarr/Services/Validation/ValidationResult.cs similarity index 97% rename from octo-fiesta/Services/Validation/ValidationResult.cs rename to allstarr/Services/Validation/ValidationResult.cs index 2a3fc16..7293f8c 100644 --- a/octo-fiesta/Services/Validation/ValidationResult.cs +++ b/allstarr/Services/Validation/ValidationResult.cs @@ -1,4 +1,4 @@ -namespace octo_fiesta.Services.Validation; +namespace allstarr.Services.Validation; /// /// Result of a startup validation operation diff --git a/octo-fiesta/octo-fiesta.csproj b/allstarr/allstarr.csproj similarity index 80% rename from octo-fiesta/octo-fiesta.csproj rename to allstarr/allstarr.csproj index ab568f4..83c5b4d 100644 --- a/octo-fiesta/octo-fiesta.csproj +++ b/allstarr/allstarr.csproj @@ -4,12 +4,13 @@ net9.0 enable enable - octo_fiesta + allstarr + diff --git a/allstarr/allstarr.http b/allstarr/allstarr.http new file mode 100644 index 0000000..1e4703a --- /dev/null +++ b/allstarr/allstarr.http @@ -0,0 +1,6 @@ +@allstarr_HostAddress = http://localhost:5274 + +GET {{allstarr_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/octo-fiesta/appsettings.Development.json b/allstarr/appsettings.Development.json similarity index 100% rename from octo-fiesta/appsettings.Development.json rename to allstarr/appsettings.Development.json diff --git a/allstarr/appsettings.json b/allstarr/appsettings.json new file mode 100644 index 0000000..8fec627 --- /dev/null +++ b/allstarr/appsettings.json @@ -0,0 +1,46 @@ +{ + "Backend": { + "Type": "Subsonic" + }, + "Subsonic": { + "Url": "https://navidrome.local.bransonb.com", + "MusicService": "SquidWTF", + "ExplicitFilter": "All", + "DownloadMode": "Track", + "StorageMode": "Permanent", + "CacheDurationHours": 1, + "EnableExternalPlaylists": true + }, + "Jellyfin": { + "Url": "http://localhost:8096", + "ApiKey": "", + "UserId": "", + "LibraryId": "", + "MusicService": "SquidWTF", + "ExplicitFilter": "All", + "DownloadMode": "Track", + "StorageMode": "Permanent", + "CacheDurationHours": 1, + "EnableExternalPlaylists": true + }, + "Library": { + "DownloadPath": "./downloads" + }, + "Qobuz": { + "UserAuthToken": "your-qobuz-token", + "UserId": "your-qobuz-user-id", + "Quality": "FLAC" + }, + "Deezer": { + "Arl": "your-deezer-arl-token", + "ArlFallback": "", + "Quality": "FLAC" + }, + "SquidWTF": { + "Quality": "FLAC" + }, + "Redis": { + "Enabled": true, + "ConnectionString": "localhost:6379" + } +} diff --git a/apis/jellyfin-openapi-stable.json b/apis/jellyfin-openapi-stable.json new file mode 100644 index 0000000..0a45a30 --- /dev/null +++ b/apis/jellyfin-openapi-stable.json @@ -0,0 +1,69435 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Jellyfin API", + "version": "10.11.6", + "x-jellyfin-version": "10.11.6" + }, + "servers": [ + { + "url": "http://localhost" + } + ], + "paths": { + "/System/ActivityLog/Entries": { + "get": { + "tags": [ + "ActivityLog" + ], + "summary": "Gets activity log entries.", + "operationId": "GetLogEntries", + "parameters": [ + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minDate", + "in": "query", + "description": "Optional. The minimum date. Format = ISO.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "hasUserId", + "in": "query", + "description": "Optional. Filter log entries if it has user id, or not.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Activity log returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivityLogEntryQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ActivityLogEntryQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ActivityLogEntryQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Auth/Keys": { + "get": { + "tags": [ + "ApiKey" + ], + "summary": "Get all keys.", + "operationId": "GetKeys", + "responses": { + "200": { + "description": "Api keys retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationInfoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/AuthenticationInfoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/AuthenticationInfoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "post": { + "tags": [ + "ApiKey" + ], + "summary": "Create a new api key.", + "operationId": "CreateKey", + "parameters": [ + { + "name": "app", + "in": "query", + "description": "Name of the app using the authentication key.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Api key created." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Auth/Keys/{key}": { + "delete": { + "tags": [ + "ApiKey" + ], + "summary": "Remove an api key.", + "operationId": "RevokeKey", + "parameters": [ + { + "name": "key", + "in": "path", + "description": "The access token to delete.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Api key deleted." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Artists": { + "get": { + "tags": [ + "Artists" + ], + "summary": "Gets all artists from a given item, folder, or the entire library.", + "operationId": "GetArtists", + "parameters": [ + { + "name": "minCommunityRating", + "in": "query", + "description": "Optional filter by minimum community rating.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "searchTerm", + "in": "query", + "description": "Optional. Search term.", + "schema": { + "type": "string" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "excludeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "filters", + "in": "query", + "description": "Optional. Specify additional filters to apply.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFilter" + } + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional filter by items that are marked as favorite, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "mediaTypes", + "in": "query", + "description": "Optional filter by MediaType. Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + } + } + }, + { + "name": "genres", + "in": "query", + "description": "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "genreIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "officialRatings", + "in": "query", + "description": "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "tags", + "in": "query", + "description": "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "years", + "in": "query", + "description": "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional, include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional, the max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "person", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified person.", + "schema": { + "type": "string" + } + }, + { + "name": "personIds", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified person ids.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "personTypes", + "in": "query", + "description": "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "studios", + "in": "query", + "description": "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "studioIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "nameStartsWithOrGreater", + "in": "query", + "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameStartsWith", + "in": "query", + "description": "Optional filter by items whose name is sorted equally than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameLessThan", + "in": "query", + "description": "Optional filter by items whose name is equally or lesser than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort Order - Ascending,Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional, include image information in output.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Total record count.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Artists returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Artists/{name}": { + "get": { + "tags": [ + "Artists" + ], + "summary": "Gets an artist by name.", + "operationId": "GetArtistByName", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Studio name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Artist returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Artists/AlbumArtists": { + "get": { + "tags": [ + "Artists" + ], + "summary": "Gets all album artists from a given item, folder, or the entire library.", + "operationId": "GetAlbumArtists", + "parameters": [ + { + "name": "minCommunityRating", + "in": "query", + "description": "Optional filter by minimum community rating.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "searchTerm", + "in": "query", + "description": "Optional. Search term.", + "schema": { + "type": "string" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "excludeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "filters", + "in": "query", + "description": "Optional. Specify additional filters to apply.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFilter" + } + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional filter by items that are marked as favorite, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "mediaTypes", + "in": "query", + "description": "Optional filter by MediaType. Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + } + } + }, + { + "name": "genres", + "in": "query", + "description": "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "genreIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "officialRatings", + "in": "query", + "description": "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "tags", + "in": "query", + "description": "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "years", + "in": "query", + "description": "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional, include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional, the max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "person", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified person.", + "schema": { + "type": "string" + } + }, + { + "name": "personIds", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified person ids.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "personTypes", + "in": "query", + "description": "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "studios", + "in": "query", + "description": "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "studioIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "nameStartsWithOrGreater", + "in": "query", + "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameStartsWith", + "in": "query", + "description": "Optional filter by items whose name is sorted equally than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameLessThan", + "in": "query", + "description": "Optional filter by items whose name is equally or lesser than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort Order - Ascending,Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional, include image information in output.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Total record count.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Album artists returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Audio/{itemId}/stream": { + "get": { + "tags": [ + "Audio" + ], + "summary": "Gets an audio stream.", + "operationId": "GetAudioStream", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "query", + "description": "The audio container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Audio stream returned.", + "content": { + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Audio" + ], + "summary": "Gets an audio stream.", + "operationId": "HeadAudioStream", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "query", + "description": "The audio container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Audio stream returned.", + "content": { + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Audio/{itemId}/stream.{container}": { + "get": { + "tags": [ + "Audio" + ], + "summary": "Gets an audio stream.", + "operationId": "GetAudioStreamByContainer", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "path", + "description": "The audio container.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Audio stream returned.", + "content": { + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Audio" + ], + "summary": "Gets an audio stream.", + "operationId": "HeadAudioStreamByContainer", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "path", + "description": "The audio container.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Audio stream returned.", + "content": { + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Backup": { + "get": { + "tags": [ + "Backup" + ], + "summary": "Gets a list of all currently present backups in the backup directory.", + "operationId": "ListBackups", + "responses": { + "200": { + "description": "Backups available.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BackupManifestDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BackupManifestDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BackupManifestDto" + } + } + } + } + }, + "403": { + "description": "User does not have permission to retrieve information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Backup/Create": { + "post": { + "tags": [ + "Backup" + ], + "summary": "Creates a new Backup.", + "operationId": "CreateBackup", + "requestBody": { + "description": "The backup options.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BackupOptionsDto" + } + ], + "description": "Defines the optional contents of the backup archive." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BackupOptionsDto" + } + ], + "description": "Defines the optional contents of the backup archive." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BackupOptionsDto" + } + ], + "description": "Defines the optional contents of the backup archive." + } + } + } + }, + "responses": { + "200": { + "description": "Backup created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BackupManifestDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BackupManifestDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BackupManifestDto" + } + } + } + }, + "403": { + "description": "User does not have permission to retrieve information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Backup/Manifest": { + "get": { + "tags": [ + "Backup" + ], + "summary": "Gets the descriptor from an existing archive is present.", + "operationId": "GetBackup", + "parameters": [ + { + "name": "path", + "in": "query", + "description": "The data to start a restore process.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Backup archive manifest.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BackupManifestDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BackupManifestDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BackupManifestDto" + } + } + } + }, + "204": { + "description": "Not a valid jellyfin Archive." + }, + "404": { + "description": "Not a valid path.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "403": { + "description": "User does not have permission to retrieve information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Backup/Restore": { + "post": { + "tags": [ + "Backup" + ], + "summary": "Restores to a backup by restarting the server and applying the backup.", + "operationId": "StartRestoreBackup", + "requestBody": { + "description": "The data to start a restore process.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BackupRestoreRequestDto" + } + ], + "description": "Defines properties used to start a restore process." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BackupRestoreRequestDto" + } + ], + "description": "Defines properties used to start a restore process." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BackupRestoreRequestDto" + } + ], + "description": "Defines properties used to start a restore process." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Backup restore started." + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "403": { + "description": "User does not have permission to retrieve information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Branding/Configuration": { + "get": { + "tags": [ + "Branding" + ], + "summary": "Gets branding configuration.", + "operationId": "GetBrandingOptions", + "responses": { + "200": { + "description": "Branding configuration returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrandingOptionsDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BrandingOptionsDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BrandingOptionsDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Branding/Css": { + "get": { + "tags": [ + "Branding" + ], + "summary": "Gets branding css.", + "operationId": "GetBrandingCss", + "responses": { + "200": { + "description": "Branding css returned.", + "content": { + "text/css": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "string" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "string" + } + } + } + }, + "204": { + "description": "No branding css configured." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Branding/Css.css": { + "get": { + "tags": [ + "Branding" + ], + "summary": "Gets branding css.", + "operationId": "GetBrandingCss_2", + "responses": { + "200": { + "description": "Branding css returned.", + "content": { + "text/css": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "string" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "string" + } + } + } + }, + "204": { + "description": "No branding css configured." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Channels": { + "get": { + "tags": [ + "Channels" + ], + "summary": "Gets available channels.", + "operationId": "GetChannels", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User Id to filter by. Use System.Guid.Empty to not filter by user.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "supportsLatestItems", + "in": "query", + "description": "Optional. Filter by channels that support getting latest items.", + "schema": { + "type": "boolean" + } + }, + { + "name": "supportsMediaDeletion", + "in": "query", + "description": "Optional. Filter by channels that support media deletion.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional. Filter by channels that are favorite.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Channels returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Channels/{channelId}/Features": { + "get": { + "tags": [ + "Channels" + ], + "summary": "Get channel features.", + "operationId": "GetChannelFeatures", + "parameters": [ + { + "name": "channelId", + "in": "path", + "description": "Channel id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Channel features returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChannelFeatures" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ChannelFeatures" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ChannelFeatures" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Channels/{channelId}/Items": { + "get": { + "tags": [ + "Channels" + ], + "summary": "Get channel items.", + "operationId": "GetChannelItems", + "parameters": [ + { + "name": "channelId", + "in": "path", + "description": "Channel Id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "folderId", + "in": "query", + "description": "Optional. Folder Id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. User Id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Optional. Sort Order - Ascending,Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + }, + { + "name": "filters", + "in": "query", + "description": "Optional. Specify additional filters to apply.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFilter" + } + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + } + ], + "responses": { + "200": { + "description": "Channel items returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Channels/Features": { + "get": { + "tags": [ + "Channels" + ], + "summary": "Get all channel features.", + "operationId": "GetAllChannelFeatures", + "responses": { + "200": { + "description": "All channel features returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelFeatures" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelFeatures" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelFeatures" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Channels/Items/Latest": { + "get": { + "tags": [ + "Channels" + ], + "summary": "Gets latest channel items.", + "operationId": "GetLatestChannelItems", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Optional. User Id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filters", + "in": "query", + "description": "Optional. Specify additional filters to apply.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFilter" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "channelIds", + "in": "query", + "description": "Optional. Specify one or more channel id's, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + ], + "responses": { + "200": { + "description": "Latest channel items returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/ClientLog/Document": { + "post": { + "tags": [ + "ClientLog" + ], + "summary": "Upload a document.", + "operationId": "LogFile", + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "200": { + "description": "Document saved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientLogDocumentResponseDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ClientLogDocumentResponseDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ClientLogDocumentResponseDto" + } + } + } + }, + "403": { + "description": "Event logging disabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "413": { + "description": "Upload size too large.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Collections": { + "post": { + "tags": [ + "Collection" + ], + "summary": "Creates a new collection.", + "operationId": "CreateCollection", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The name of the collection.", + "schema": { + "type": "string" + } + }, + { + "name": "ids", + "in": "query", + "description": "Item Ids to add to the collection.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "parentId", + "in": "query", + "description": "Optional. Create the collection within a specific folder.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "isLocked", + "in": "query", + "description": "Whether or not to lock the new collection.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Collection created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionCreationResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/CollectionCreationResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/CollectionCreationResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "CollectionManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Collections/{collectionId}/Items": { + "post": { + "tags": [ + "Collection" + ], + "summary": "Adds items to a collection.", + "operationId": "AddToCollection", + "parameters": [ + { + "name": "collectionId", + "in": "path", + "description": "The collection id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "ids", + "in": "query", + "description": "Item ids, comma delimited.", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + ], + "responses": { + "204": { + "description": "Items added to collection." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "CollectionManagement", + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "Collection" + ], + "summary": "Removes items from a collection.", + "operationId": "RemoveFromCollection", + "parameters": [ + { + "name": "collectionId", + "in": "path", + "description": "The collection id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "ids", + "in": "query", + "description": "Item ids, comma delimited.", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + ], + "responses": { + "204": { + "description": "Items removed from collection." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "CollectionManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/System/Configuration": { + "get": { + "tags": [ + "Configuration" + ], + "summary": "Gets application configuration.", + "operationId": "GetConfiguration", + "responses": { + "200": { + "description": "Application configuration returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerConfiguration" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ServerConfiguration" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ServerConfiguration" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "Configuration" + ], + "summary": "Updates application configuration.", + "operationId": "UpdateConfiguration", + "requestBody": { + "description": "Configuration.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ServerConfiguration" + } + ], + "description": "Represents the server configuration." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ServerConfiguration" + } + ], + "description": "Represents the server configuration." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ServerConfiguration" + } + ], + "description": "Represents the server configuration." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Configuration updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation", + "DefaultAuthorization" + ] + } + ] + } + }, + "/System/Configuration/{key}": { + "get": { + "tags": [ + "Configuration" + ], + "summary": "Gets a named configuration.", + "operationId": "GetNamedConfiguration", + "parameters": [ + { + "name": "key", + "in": "path", + "description": "Configuration key.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Configuration returned.", + "content": { + "application/json": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "Configuration" + ], + "summary": "Updates named configuration.", + "operationId": "UpdateNamedConfiguration", + "parameters": [ + { + "name": "key", + "in": "path", + "description": "Configuration key.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Configuration.", + "content": { + "application/json": { + "schema": { } + }, + "text/json": { + "schema": { } + }, + "application/*+json": { + "schema": { } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Named configuration updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation", + "DefaultAuthorization" + ] + } + ] + } + }, + "/System/Configuration/Branding": { + "post": { + "tags": [ + "Configuration" + ], + "summary": "Updates branding configuration.", + "operationId": "UpdateBrandingConfiguration", + "requestBody": { + "description": "Branding configuration.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BrandingOptionsDto" + } + ], + "description": "The branding options DTO for API use.\r\nThis DTO excludes SplashscreenLocation to prevent it from being updated via API." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BrandingOptionsDto" + } + ], + "description": "The branding options DTO for API use.\r\nThis DTO excludes SplashscreenLocation to prevent it from being updated via API." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BrandingOptionsDto" + } + ], + "description": "The branding options DTO for API use.\r\nThis DTO excludes SplashscreenLocation to prevent it from being updated via API." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Branding configuration updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation", + "DefaultAuthorization" + ] + } + ] + } + }, + "/System/Configuration/MetadataOptions/Default": { + "get": { + "tags": [ + "Configuration" + ], + "summary": "Gets a default MetadataOptions object.", + "operationId": "GetDefaultMetadataOptions", + "responses": { + "200": { + "description": "Metadata options returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataOptions" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/MetadataOptions" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/MetadataOptions" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation", + "DefaultAuthorization" + ] + } + ] + } + }, + "/web/ConfigurationPage": { + "get": { + "tags": [ + "Dashboard" + ], + "summary": "Gets a dashboard configuration page.", + "operationId": "GetDashboardConfigurationPage", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The name of the page.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ConfigurationPage returned.", + "content": { + "text/html": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "application/x-javascript": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Plugin configuration page not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/web/ConfigurationPages": { + "get": { + "tags": [ + "Dashboard" + ], + "summary": "Gets the configuration pages.", + "operationId": "GetConfigurationPages", + "parameters": [ + { + "name": "enableInMainMenu", + "in": "query", + "description": "Whether to enable in the main menu.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "ConfigurationPages returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigurationPageInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigurationPageInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigurationPageInfo" + } + } + } + } + }, + "404": { + "description": "Server still loading.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Devices": { + "get": { + "tags": [ + "Devices" + ], + "summary": "Get Devices.", + "operationId": "GetDevices", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Gets or sets the user identifier.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Devices retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceInfoDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/DeviceInfoDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/DeviceInfoDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "delete": { + "tags": [ + "Devices" + ], + "summary": "Deletes a device.", + "operationId": "DeleteDevice", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "Device Id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Device deleted." + }, + "404": { + "description": "Device not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Devices/Info": { + "get": { + "tags": [ + "Devices" + ], + "summary": "Get info for a device.", + "operationId": "GetDeviceInfo", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "Device Id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Device info retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceInfoDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/DeviceInfoDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/DeviceInfoDto" + } + } + } + }, + "404": { + "description": "Device not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Devices/Options": { + "get": { + "tags": [ + "Devices" + ], + "summary": "Get options for a device.", + "operationId": "GetDeviceOptions", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "Device Id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Device options retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceOptionsDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/DeviceOptionsDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/DeviceOptionsDto" + } + } + } + }, + "404": { + "description": "Device not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "post": { + "tags": [ + "Devices" + ], + "summary": "Update device options.", + "operationId": "UpdateDeviceOptions", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "Device Id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Device Options.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/DeviceOptionsDto" + } + ], + "description": "A dto representing custom options for a device." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/DeviceOptionsDto" + } + ], + "description": "A dto representing custom options for a device." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/DeviceOptionsDto" + } + ], + "description": "A dto representing custom options for a device." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Device options updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/DisplayPreferences/{displayPreferencesId}": { + "get": { + "tags": [ + "DisplayPreferences" + ], + "summary": "Get Display Preferences.", + "operationId": "GetDisplayPreferences", + "parameters": [ + { + "name": "displayPreferencesId", + "in": "path", + "description": "Display preferences id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "client", + "in": "query", + "description": "Client.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Display preferences retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DisplayPreferencesDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/DisplayPreferencesDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/DisplayPreferencesDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "DisplayPreferences" + ], + "summary": "Update Display Preferences.", + "operationId": "UpdateDisplayPreferences", + "parameters": [ + { + "name": "displayPreferencesId", + "in": "path", + "description": "Display preferences id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "User Id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "client", + "in": "query", + "description": "Client.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "New Display Preferences object.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/DisplayPreferencesDto" + } + ], + "description": "Defines the display preferences for any item that supports them (usually Folders)." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/DisplayPreferencesDto" + } + ], + "description": "Defines the display preferences for any item that supports them (usually Folders)." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/DisplayPreferencesDto" + } + ], + "description": "Defines the display preferences for any item that supports them (usually Folders)." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Display preferences updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}": { + "get": { + "tags": [ + "DynamicHls" + ], + "summary": "Gets a video stream using HTTP live streaming.", + "operationId": "GetHlsAudioSegment", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "segmentId", + "in": "path", + "description": "The segment id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "container", + "in": "path", + "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "runtimeTicks", + "in": "query", + "description": "The position of the requested segment in ticks.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "actualSegmentLengthTicks", + "in": "query", + "description": "The length of the requested segment in ticks.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxStreamingBitrate", + "in": "query", + "description": "Optional. The maximum streaming bitrate.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Video stream returned.", + "content": { + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Audio/{itemId}/main.m3u8": { + "get": { + "tags": [ + "DynamicHls" + ], + "summary": "Gets an audio stream using HTTP live streaming.", + "operationId": "GetVariantHlsAudioPlaylist", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxStreamingBitrate", + "in": "query", + "description": "Optional. The maximum streaming bitrate.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Audio stream returned.", + "content": { + "application/x-mpegURL": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Audio/{itemId}/master.m3u8": { + "get": { + "tags": [ + "DynamicHls" + ], + "summary": "Gets an audio hls playlist stream.", + "operationId": "GetMasterHlsAudioPlaylist", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxStreamingBitrate", + "in": "query", + "description": "Optional. The maximum streaming bitrate.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAdaptiveBitrateStreaming", + "in": "query", + "description": "Enable adaptive bitrate streaming.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Audio stream returned.", + "content": { + "application/x-mpegURL": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "head": { + "tags": [ + "DynamicHls" + ], + "summary": "Gets an audio hls playlist stream.", + "operationId": "HeadMasterHlsAudioPlaylist", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxStreamingBitrate", + "in": "query", + "description": "Optional. The maximum streaming bitrate.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAdaptiveBitrateStreaming", + "in": "query", + "description": "Enable adaptive bitrate streaming.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Audio stream returned.", + "content": { + "application/x-mpegURL": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}": { + "get": { + "tags": [ + "DynamicHls" + ], + "summary": "Gets a video stream using HTTP live streaming.", + "operationId": "GetHlsVideoSegment", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "segmentId", + "in": "path", + "description": "The segment id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "container", + "in": "path", + "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "runtimeTicks", + "in": "query", + "description": "The position of the requested segment in ticks.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "actualSegmentLengthTicks", + "in": "query", + "description": "The length of the requested segment in ticks.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The desired segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. The maximum horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. The maximum vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "alwaysBurnInSubtitleWhenTranscoding", + "in": "query", + "description": "Whether to always burn in subtitles when transcoding.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Video stream returned.", + "content": { + "video/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{itemId}/live.m3u8": { + "get": { + "tags": [ + "DynamicHls" + ], + "summary": "Gets a hls live stream.", + "operationId": "GetLiveHlsStream", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "query", + "description": "The audio container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. The max width.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. The max height.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableSubtitlesInManifest", + "in": "query", + "description": "Optional. Whether to enable subtitles in the manifest.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "alwaysBurnInSubtitleWhenTranscoding", + "in": "query", + "description": "Whether to always burn in subtitles when transcoding.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Hls live stream retrieved.", + "content": { + "application/x-mpegURL": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{itemId}/main.m3u8": { + "get": { + "tags": [ + "DynamicHls" + ], + "summary": "Gets a video stream using HTTP live streaming.", + "operationId": "GetVariantHlsVideoPlaylist", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. The maximum horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. The maximum vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "alwaysBurnInSubtitleWhenTranscoding", + "in": "query", + "description": "Whether to always burn in subtitles when transcoding.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Video stream returned.", + "content": { + "application/x-mpegURL": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{itemId}/master.m3u8": { + "get": { + "tags": [ + "DynamicHls" + ], + "summary": "Gets a video hls playlist stream.", + "operationId": "GetMasterHlsVideoPlaylist", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. The maximum horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. The maximum vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAdaptiveBitrateStreaming", + "in": "query", + "description": "Enable adaptive bitrate streaming.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "enableTrickplay", + "in": "query", + "description": "Enable trickplay image playlists being added to master playlist.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "alwaysBurnInSubtitleWhenTranscoding", + "in": "query", + "description": "Whether to always burn in subtitles when transcoding.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Video stream returned.", + "content": { + "application/x-mpegURL": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "head": { + "tags": [ + "DynamicHls" + ], + "summary": "Gets a video hls playlist stream.", + "operationId": "HeadMasterHlsVideoPlaylist", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. The maximum horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. The maximum vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAdaptiveBitrateStreaming", + "in": "query", + "description": "Enable adaptive bitrate streaming.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "enableTrickplay", + "in": "query", + "description": "Enable trickplay image playlists being added to master playlist.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "alwaysBurnInSubtitleWhenTranscoding", + "in": "query", + "description": "Whether to always burn in subtitles when transcoding.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Video stream returned.", + "content": { + "application/x-mpegURL": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Environment/DefaultDirectoryBrowser": { + "get": { + "tags": [ + "Environment" + ], + "summary": "Get Default directory browser.", + "operationId": "GetDefaultDirectoryBrowser", + "responses": { + "200": { + "description": "Default directory browser returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DefaultDirectoryBrowserInfoDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/DefaultDirectoryBrowserInfoDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/DefaultDirectoryBrowserInfoDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Environment/DirectoryContents": { + "get": { + "tags": [ + "Environment" + ], + "summary": "Gets the contents of a given directory in the file system.", + "operationId": "GetDirectoryContents", + "parameters": [ + { + "name": "path", + "in": "query", + "description": "The path.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "includeFiles", + "in": "query", + "description": "An optional filter to include or exclude files from the results. true/false.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "includeDirectories", + "in": "query", + "description": "An optional filter to include or exclude folders from the results. true/false.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Directory contents returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemEntryInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemEntryInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemEntryInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Environment/Drives": { + "get": { + "tags": [ + "Environment" + ], + "summary": "Gets available drives from the server's file system.", + "operationId": "GetDrives", + "responses": { + "200": { + "description": "List of entries returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemEntryInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemEntryInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemEntryInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Environment/NetworkShares": { + "get": { + "tags": [ + "Environment" + ], + "summary": "Gets network paths.", + "operationId": "GetNetworkShares", + "responses": { + "200": { + "description": "Empty array returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemEntryInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemEntryInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemEntryInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Environment/ParentPath": { + "get": { + "tags": [ + "Environment" + ], + "summary": "Gets the parent path of a given path.", + "operationId": "GetParentPath", + "parameters": [ + { + "name": "path", + "in": "query", + "description": "The path.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "string" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "string" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Environment/ValidatePath": { + "post": { + "tags": [ + "Environment" + ], + "summary": "Validates path.", + "operationId": "ValidatePath", + "requestBody": { + "description": "Validate request object.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ValidatePathDto" + } + ], + "description": "Validate path object." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ValidatePathDto" + } + ], + "description": "Validate path object." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ValidatePathDto" + } + ], + "description": "Validate path object." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Path validated." + }, + "404": { + "description": "Path not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/Filters": { + "get": { + "tags": [ + "Filter" + ], + "summary": "Gets legacy query filters.", + "operationId": "GetQueryFiltersLegacy", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Optional. User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Optional. Parent id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "mediaTypes", + "in": "query", + "description": "Optional. Filter by MediaType. Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + } + } + } + ], + "responses": { + "200": { + "description": "Legacy filters retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryFiltersLegacy" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/QueryFiltersLegacy" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/QueryFiltersLegacy" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/Filters2": { + "get": { + "tags": [ + "Filter" + ], + "summary": "Gets query filters.", + "operationId": "GetQueryFilters", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Optional. User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "isAiring", + "in": "query", + "description": "Optional. Is item airing.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMovie", + "in": "query", + "description": "Optional. Is item movie.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSports", + "in": "query", + "description": "Optional. Is item sports.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isKids", + "in": "query", + "description": "Optional. Is item kids.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isNews", + "in": "query", + "description": "Optional. Is item news.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSeries", + "in": "query", + "description": "Optional. Is item series.", + "schema": { + "type": "boolean" + } + }, + { + "name": "recursive", + "in": "query", + "description": "Optional. Search recursive.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Filters retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryFilters" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/QueryFilters" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/QueryFilters" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Genres": { + "get": { + "tags": [ + "Genres" + ], + "summary": "Gets all genres from a given item, folder, or the entire library.", + "operationId": "GetGenres", + "parameters": [ + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "searchTerm", + "in": "query", + "description": "The search term.", + "schema": { + "type": "string" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "excludeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional filter by items that are marked as favorite, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional, the max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "nameStartsWithOrGreater", + "in": "query", + "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameStartsWith", + "in": "query", + "description": "Optional filter by items whose name is sorted equally than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameLessThan", + "in": "query", + "description": "Optional filter by items whose name is equally or lesser than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort Order - Ascending,Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional, include image information in output.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Optional. Include total record count.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Genres returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Genres/{genreName}": { + "get": { + "tags": [ + "Genres" + ], + "summary": "Gets a genre, by name.", + "operationId": "GetGenre", + "parameters": [ + { + "name": "genreName", + "in": "path", + "description": "The genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Genres returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Audio/{itemId}/hls/{segmentId}/stream.aac": { + "get": { + "tags": [ + "HlsSegment" + ], + "summary": "Gets the specified audio segment for an audio item.", + "operationId": "GetHlsAudioSegmentLegacyAac", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "segmentId", + "in": "path", + "description": "The segment id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Hls audio segment returned.", + "content": { + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Audio/{itemId}/hls/{segmentId}/stream.mp3": { + "get": { + "tags": [ + "HlsSegment" + ], + "summary": "Gets the specified audio segment for an audio item.", + "operationId": "GetHlsAudioSegmentLegacyMp3", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "segmentId", + "in": "path", + "description": "The segment id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Hls audio segment returned.", + "content": { + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}": { + "get": { + "tags": [ + "HlsSegment" + ], + "summary": "Gets a hls video segment.", + "operationId": "GetHlsVideoSegmentLegacy", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "segmentId", + "in": "path", + "description": "The segment id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "path", + "description": "The segment container.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Hls video segment returned.", + "content": { + "video/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Hls segment not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Videos/{itemId}/hls/{playlistId}/stream.m3u8": { + "get": { + "tags": [ + "HlsSegment" + ], + "summary": "Gets a hls video playlist.", + "operationId": "GetHlsPlaylistLegacy", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The video id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Hls video playlist returned.", + "content": { + "application/x-mpegURL": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/ActiveEncodings": { + "delete": { + "tags": [ + "HlsSegment" + ], + "summary": "Stops an active encoding.", + "operationId": "StopEncodingProcess", + "parameters": [ + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Encoding stopped successfully." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Artists/{name}/Images/{imageType}/{imageIndex}": { + "get": { + "tags": [ + "Image" + ], + "summary": "Get artist image by name.", + "operationId": "GetArtistImage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Artist name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Get artist image by name.", + "operationId": "HeadArtistImage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Artist name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Branding/Splashscreen": { + "get": { + "tags": [ + "Image" + ], + "summary": "Generates or gets the splashscreen.", + "operationId": "GetSplashscreen", + "parameters": [ + { + "name": "tag", + "in": "query", + "description": "Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + } + ], + "responses": { + "200": { + "description": "Splashscreen returned successfully.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "post": { + "tags": [ + "Image" + ], + "summary": "Uploads a custom splashscreen.\r\nThe body is expected to the image contents base64 encoded.", + "operationId": "UploadCustomSplashscreen", + "requestBody": { + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "204": { + "description": "Successfully uploaded new splashscreen." + }, + "400": { + "description": "Error reading MimeType from uploaded image.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "403": { + "description": "User does not have permission to upload splashscreen..", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "delete": { + "tags": [ + "Image" + ], + "summary": "Delete a custom splashscreen.", + "operationId": "DeleteCustomSplashscreen", + "responses": { + "204": { + "description": "Successfully deleted the custom splashscreen." + }, + "403": { + "description": "User does not have permission to delete splashscreen.." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Genres/{name}/Images/{imageType}": { + "get": { + "tags": [ + "Image" + ], + "summary": "Get genre image by name.", + "operationId": "GetGenreImage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "Image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Get genre image by name.", + "operationId": "HeadGenreImage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "Image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Genres/{name}/Images/{imageType}/{imageIndex}": { + "get": { + "tags": [ + "Image" + ], + "summary": "Get genre image by name.", + "operationId": "GetGenreImageByIndex", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Get genre image by name.", + "operationId": "HeadGenreImageByIndex", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Items/{itemId}/Images": { + "get": { + "tags": [ + "Image" + ], + "summary": "Get item image infos.", + "operationId": "GetItemImageInfos", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Item images returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageInfo" + } + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/Images/{imageType}": { + "delete": { + "tags": [ + "Image" + ], + "summary": "Delete an item's image.", + "operationId": "DeleteItemImage", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "The image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "Image deleted." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "post": { + "tags": [ + "Image" + ], + "summary": "Set item image.", + "operationId": "SetItemImage", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + } + ], + "requestBody": { + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "204": { + "description": "Image saved." + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "get": { + "tags": [ + "Image" + ], + "summary": "Gets the item's image.", + "operationId": "GetItemImage", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Drawing.ImageFormat of the returned image.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "Image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Gets the item's image.", + "operationId": "HeadItemImage", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Drawing.ImageFormat of the returned image.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "Image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Items/{itemId}/Images/{imageType}/{imageIndex}": { + "delete": { + "tags": [ + "Image" + ], + "summary": "Delete an item's image.", + "operationId": "DeleteItemImageByIndex", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "The image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "Image deleted." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "post": { + "tags": [ + "Image" + ], + "summary": "Set item image.", + "operationId": "SetItemImageByIndex", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "(Unused) Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "204": { + "description": "Image saved." + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "get": { + "tags": [ + "Image" + ], + "summary": "Gets the item's image.", + "operationId": "GetItemImageByIndex", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Drawing.ImageFormat of the returned image.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Gets the item's image.", + "operationId": "HeadItemImageByIndex", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Drawing.ImageFormat of the returned image.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}": { + "get": { + "tags": [ + "Image" + ], + "summary": "Gets the item's image.", + "operationId": "GetItemImage2", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "maxWidth", + "in": "path", + "description": "The maximum image width to return.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "path", + "description": "The maximum image height to return.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "path", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "path", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "required": true, + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ], + "description": "Enum ImageOutputFormat." + } + }, + { + "name": "percentPlayed", + "in": "path", + "description": "Optional. Percent to render for the percent played overlay.", + "required": true, + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "path", + "description": "Optional. Unplayed count overlay to render.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Gets the item's image.", + "operationId": "HeadItemImage2", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "maxWidth", + "in": "path", + "description": "The maximum image width to return.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "path", + "description": "The maximum image height to return.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "path", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "path", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "required": true, + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ], + "description": "Enum ImageOutputFormat." + } + }, + { + "name": "percentPlayed", + "in": "path", + "description": "Optional. Percent to render for the percent played overlay.", + "required": true, + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "path", + "description": "Optional. Unplayed count overlay to render.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Items/{itemId}/Images/{imageType}/{imageIndex}/Index": { + "post": { + "tags": [ + "Image" + ], + "summary": "Updates the index for an item image.", + "operationId": "UpdateItemImageIndex", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Old image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "newIndex", + "in": "query", + "description": "New image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "Image index updated." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/MusicGenres/{name}/Images/{imageType}": { + "get": { + "tags": [ + "Image" + ], + "summary": "Get music genre image by name.", + "operationId": "GetMusicGenreImage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Music genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "Image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Get music genre image by name.", + "operationId": "HeadMusicGenreImage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Music genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "Image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/MusicGenres/{name}/Images/{imageType}/{imageIndex}": { + "get": { + "tags": [ + "Image" + ], + "summary": "Get music genre image by name.", + "operationId": "GetMusicGenreImageByIndex", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Music genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Get music genre image by name.", + "operationId": "HeadMusicGenreImageByIndex", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Music genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Persons/{name}/Images/{imageType}": { + "get": { + "tags": [ + "Image" + ], + "summary": "Get person image by name.", + "operationId": "GetPersonImage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Person name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "Image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Get person image by name.", + "operationId": "HeadPersonImage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Person name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "Image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Persons/{name}/Images/{imageType}/{imageIndex}": { + "get": { + "tags": [ + "Image" + ], + "summary": "Get person image by name.", + "operationId": "GetPersonImageByIndex", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Person name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Get person image by name.", + "operationId": "HeadPersonImageByIndex", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Person name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Studios/{name}/Images/{imageType}": { + "get": { + "tags": [ + "Image" + ], + "summary": "Get studio image by name.", + "operationId": "GetStudioImage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Studio name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "Image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Get studio image by name.", + "operationId": "HeadStudioImage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Studio name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + }, + { + "name": "imageIndex", + "in": "query", + "description": "Image index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Studios/{name}/Images/{imageType}/{imageIndex}": { + "get": { + "tags": [ + "Image" + ], + "summary": "Get studio image by name.", + "operationId": "GetStudioImageByIndex", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Studio name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Get studio image by name.", + "operationId": "HeadStudioImageByIndex", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Studio name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "imageType", + "in": "path", + "description": "Image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageIndex", + "in": "path", + "description": "Image index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "The maximum image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "The maximum image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "percentPlayed", + "in": "query", + "description": "Optional. Percent to render for the percent played overlay.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "unplayedCount", + "in": "query", + "description": "Optional. Unplayed count overlay to render.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "description": "The fixed image width to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "The fixed image height to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "quality", + "in": "query", + "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillWidth", + "in": "query", + "description": "Width of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fillHeight", + "in": "query", + "description": "Height of box to fill.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "blur", + "in": "query", + "description": "Optional. Blur image.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "backgroundColor", + "in": "query", + "description": "Optional. Apply a background color for transparent images.", + "schema": { + "type": "string" + } + }, + { + "name": "foregroundLayer", + "in": "query", + "description": "Optional. Apply a foreground layer on top of the image.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/UserImage": { + "post": { + "tags": [ + "Image" + ], + "summary": "Sets the user image.", + "operationId": "PostUserImage", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User Id.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "204": { + "description": "Image updated." + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "403": { + "description": "User does not have permission to delete the image.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "Image" + ], + "summary": "Delete the user's image.", + "operationId": "DeleteUserImage", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User Id.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Image deleted." + }, + "403": { + "description": "User does not have permission to delete the image.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "get": { + "tags": [ + "Image" + ], + "summary": "Get user profile image.", + "operationId": "GetUserImage", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "User id not provided.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Image" + ], + "summary": "Get user profile image.", + "operationId": "HeadUserImage", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "tag", + "in": "query", + "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "query", + "description": "Determines the output format of the image - original,gif,jpg,png.", + "schema": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + } + } + ], + "responses": { + "200": { + "description": "Image stream returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "User id not provided.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Albums/{itemId}/InstantMix": { + "get": { + "tags": [ + "InstantMix" + ], + "summary": "Creates an instant playlist based on a given album.", + "operationId": "GetInstantMixFromAlbum", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + } + ], + "responses": { + "200": { + "description": "Instant playlist returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Artists/{itemId}/InstantMix": { + "get": { + "tags": [ + "InstantMix" + ], + "summary": "Creates an instant playlist based on a given artist.", + "operationId": "GetInstantMixFromArtists", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + } + ], + "responses": { + "200": { + "description": "Instant playlist returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Artists/InstantMix": { + "get": { + "tags": [ + "InstantMix" + ], + "summary": "Creates an instant playlist based on a given artist.", + "operationId": "GetInstantMixFromArtists2", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + } + ], + "responses": { + "200": { + "description": "Instant playlist returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/InstantMix": { + "get": { + "tags": [ + "InstantMix" + ], + "summary": "Creates an instant playlist based on a given item.", + "operationId": "GetInstantMixFromItem", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + } + ], + "responses": { + "200": { + "description": "Instant playlist returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/MusicGenres/{name}/InstantMix": { + "get": { + "tags": [ + "InstantMix" + ], + "summary": "Creates an instant playlist based on a given genre.", + "operationId": "GetInstantMixFromMusicGenreByName", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "The genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + } + ], + "responses": { + "200": { + "description": "Instant playlist returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/MusicGenres/InstantMix": { + "get": { + "tags": [ + "InstantMix" + ], + "summary": "Creates an instant playlist based on a given genre.", + "operationId": "GetInstantMixFromMusicGenreById", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + } + ], + "responses": { + "200": { + "description": "Instant playlist returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Playlists/{itemId}/InstantMix": { + "get": { + "tags": [ + "InstantMix" + ], + "summary": "Creates an instant playlist based on a given playlist.", + "operationId": "GetInstantMixFromPlaylist", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + } + ], + "responses": { + "200": { + "description": "Instant playlist returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Songs/{itemId}/InstantMix": { + "get": { + "tags": [ + "InstantMix" + ], + "summary": "Creates an instant playlist based on a given song.", + "operationId": "GetInstantMixFromSong", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + } + ], + "responses": { + "200": { + "description": "Instant playlist returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/ExternalIdInfos": { + "get": { + "tags": [ + "ItemLookup" + ], + "summary": "Get the item's external id info.", + "operationId": "GetExternalIdInfos", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "External id info retrieved.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalIdInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalIdInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalIdInfo" + } + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/RemoteSearch/Apply/{itemId}": { + "post": { + "tags": [ + "ItemLookup" + ], + "summary": "Applies search criteria to an item and refreshes metadata.", + "operationId": "ApplySearchCriteria", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "replaceAllImages", + "in": "query", + "description": "Optional. Whether or not to replace all images. Default: True.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "requestBody": { + "description": "The remote search result.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/RemoteSearchResult" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/RemoteSearchResult" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/RemoteSearchResult" + } + ] + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Item metadata refreshed." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/RemoteSearch/Book": { + "post": { + "tags": [ + "ItemLookup" + ], + "summary": "Get book remote search.", + "operationId": "GetBookRemoteSearchResults", + "requestBody": { + "description": "Remote search query.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BookInfoRemoteSearchQuery" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BookInfoRemoteSearchQuery" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BookInfoRemoteSearchQuery" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Book remote search executed.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/RemoteSearch/BoxSet": { + "post": { + "tags": [ + "ItemLookup" + ], + "summary": "Get box set remote search.", + "operationId": "GetBoxSetRemoteSearchResults", + "requestBody": { + "description": "Remote search query.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BoxSetInfoRemoteSearchQuery" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BoxSetInfoRemoteSearchQuery" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BoxSetInfoRemoteSearchQuery" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Box set remote search executed.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/RemoteSearch/Movie": { + "post": { + "tags": [ + "ItemLookup" + ], + "summary": "Get movie remote search.", + "operationId": "GetMovieRemoteSearchResults", + "requestBody": { + "description": "Remote search query.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MovieInfoRemoteSearchQuery" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MovieInfoRemoteSearchQuery" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MovieInfoRemoteSearchQuery" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Movie remote search executed.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/RemoteSearch/MusicAlbum": { + "post": { + "tags": [ + "ItemLookup" + ], + "summary": "Get music album remote search.", + "operationId": "GetMusicAlbumRemoteSearchResults", + "requestBody": { + "description": "Remote search query.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/AlbumInfoRemoteSearchQuery" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/AlbumInfoRemoteSearchQuery" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/AlbumInfoRemoteSearchQuery" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Music album remote search executed.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/RemoteSearch/MusicArtist": { + "post": { + "tags": [ + "ItemLookup" + ], + "summary": "Get music artist remote search.", + "operationId": "GetMusicArtistRemoteSearchResults", + "requestBody": { + "description": "Remote search query.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ArtistInfoRemoteSearchQuery" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ArtistInfoRemoteSearchQuery" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ArtistInfoRemoteSearchQuery" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Music artist remote search executed.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/RemoteSearch/MusicVideo": { + "post": { + "tags": [ + "ItemLookup" + ], + "summary": "Get music video remote search.", + "operationId": "GetMusicVideoRemoteSearchResults", + "requestBody": { + "description": "Remote search query.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MusicVideoInfoRemoteSearchQuery" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MusicVideoInfoRemoteSearchQuery" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MusicVideoInfoRemoteSearchQuery" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Music video remote search executed.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/RemoteSearch/Person": { + "post": { + "tags": [ + "ItemLookup" + ], + "summary": "Get person remote search.", + "operationId": "GetPersonRemoteSearchResults", + "requestBody": { + "description": "Remote search query.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PersonLookupInfoRemoteSearchQuery" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PersonLookupInfoRemoteSearchQuery" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PersonLookupInfoRemoteSearchQuery" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Person remote search executed.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/RemoteSearch/Series": { + "post": { + "tags": [ + "ItemLookup" + ], + "summary": "Get series remote search.", + "operationId": "GetSeriesRemoteSearchResults", + "requestBody": { + "description": "Remote search query.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeriesInfoRemoteSearchQuery" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeriesInfoRemoteSearchQuery" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeriesInfoRemoteSearchQuery" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Series remote search executed.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/RemoteSearch/Trailer": { + "post": { + "tags": [ + "ItemLookup" + ], + "summary": "Get trailer remote search.", + "operationId": "GetTrailerRemoteSearchResults", + "requestBody": { + "description": "Remote search query.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TrailerInfoRemoteSearchQuery" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TrailerInfoRemoteSearchQuery" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TrailerInfoRemoteSearchQuery" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Trailer remote search executed.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/Refresh": { + "post": { + "tags": [ + "ItemRefresh" + ], + "summary": "Refreshes metadata for an item.", + "operationId": "RefreshItem", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "metadataRefreshMode", + "in": "query", + "description": "(Optional) Specifies the metadata refresh mode.", + "schema": { + "enum": [ + "None", + "ValidationOnly", + "Default", + "FullRefresh" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MetadataRefreshMode" + } + ], + "default": "None" + } + }, + { + "name": "imageRefreshMode", + "in": "query", + "description": "(Optional) Specifies the image refresh mode.", + "schema": { + "enum": [ + "None", + "ValidationOnly", + "Default", + "FullRefresh" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MetadataRefreshMode" + } + ], + "default": "None" + } + }, + { + "name": "replaceAllMetadata", + "in": "query", + "description": "(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "replaceAllImages", + "in": "query", + "description": "(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "regenerateTrickplay", + "in": "query", + "description": "(Optional) Determines if trickplay images should be replaced. Only applicable if mode is FullRefresh.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "204": { + "description": "Item metadata refresh queued." + }, + "404": { + "description": "Item to refresh not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Items": { + "get": { + "tags": [ + "Items" + ], + "summary": "Gets items based on a query.", + "operationId": "GetItems", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id supplied as query parameter; this is required when not using an API key.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "maxOfficialRating", + "in": "query", + "description": "Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).", + "schema": { + "type": "string" + } + }, + { + "name": "hasThemeSong", + "in": "query", + "description": "Optional filter by items with theme songs.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasThemeVideo", + "in": "query", + "description": "Optional filter by items with theme videos.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasSubtitles", + "in": "query", + "description": "Optional filter by items with subtitles.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasSpecialFeature", + "in": "query", + "description": "Optional filter by items with special features.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasTrailer", + "in": "query", + "description": "Optional filter by items with trailers.", + "schema": { + "type": "boolean" + } + }, + { + "name": "adjacentTo", + "in": "query", + "description": "Optional. Return items that are siblings of a supplied item.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "indexNumber", + "in": "query", + "description": "Optional filter by index number.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "parentIndexNumber", + "in": "query", + "description": "Optional filter by parent index number.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "hasParentalRating", + "in": "query", + "description": "Optional filter by items that have or do not have a parental rating.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isHd", + "in": "query", + "description": "Optional filter by items that are HD or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "is4K", + "in": "query", + "description": "Optional filter by items that are 4K or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "locationTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocationType" + } + } + }, + { + "name": "excludeLocationTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocationType" + } + } + }, + { + "name": "isMissing", + "in": "query", + "description": "Optional filter by items that are missing episodes or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isUnaired", + "in": "query", + "description": "Optional filter by items that are unaired episodes or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "minCommunityRating", + "in": "query", + "description": "Optional filter by minimum community rating.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "minCriticRating", + "in": "query", + "description": "Optional filter by minimum critic rating.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "minPremiereDate", + "in": "query", + "description": "Optional. The minimum premiere date. Format = ISO.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "minDateLastSaved", + "in": "query", + "description": "Optional. The minimum last saved date. Format = ISO.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "minDateLastSavedForUser", + "in": "query", + "description": "Optional. The minimum last saved date for the current user. Format = ISO.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "maxPremiereDate", + "in": "query", + "description": "Optional. The maximum premiere date. Format = ISO.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "hasOverview", + "in": "query", + "description": "Optional filter by items that have an overview or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasImdbId", + "in": "query", + "description": "Optional filter by items that have an IMDb id or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasTmdbId", + "in": "query", + "description": "Optional filter by items that have a TMDb id or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasTvdbId", + "in": "query", + "description": "Optional filter by items that have a TVDb id or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMovie", + "in": "query", + "description": "Optional filter for live tv movies.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSeries", + "in": "query", + "description": "Optional filter for live tv series.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isNews", + "in": "query", + "description": "Optional filter for live tv news.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isKids", + "in": "query", + "description": "Optional filter for live tv kids.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSports", + "in": "query", + "description": "Optional filter for live tv sports.", + "schema": { + "type": "boolean" + } + }, + { + "name": "excludeItemIds", + "in": "query", + "description": "Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "recursive", + "in": "query", + "description": "When searching within folders, this determines whether or not the search will be recursive. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "searchTerm", + "in": "query", + "description": "Optional. Filter based on a search term.", + "schema": { + "type": "string" + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort Order - Ascending, Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "excludeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "filters", + "in": "query", + "description": "Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFilter" + } + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional filter by items that are marked as favorite, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "mediaTypes", + "in": "query", + "description": "Optional filter by MediaType. Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + } + } + }, + { + "name": "imageTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "isPlayed", + "in": "query", + "description": "Optional filter by items that are played, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "genres", + "in": "query", + "description": "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "officialRatings", + "in": "query", + "description": "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "tags", + "in": "query", + "description": "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "years", + "in": "query", + "description": "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional, include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional, the max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "person", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified person.", + "schema": { + "type": "string" + } + }, + { + "name": "personIds", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified person id.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "personTypes", + "in": "query", + "description": "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "studios", + "in": "query", + "description": "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "artists", + "in": "query", + "description": "Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "excludeArtistIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "artistIds", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified artist id.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "albumArtistIds", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified album artist id.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "contributingArtistIds", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "albums", + "in": "query", + "description": "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "albumIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "ids", + "in": "query", + "description": "Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "videoTypes", + "in": "query", + "description": "Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VideoType" + } + } + }, + { + "name": "minOfficialRating", + "in": "query", + "description": "Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).", + "schema": { + "type": "string" + } + }, + { + "name": "isLocked", + "in": "query", + "description": "Optional filter by items that are locked.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isPlaceHolder", + "in": "query", + "description": "Optional filter by items that are placeholders.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasOfficialRating", + "in": "query", + "description": "Optional filter by items that have official ratings.", + "schema": { + "type": "boolean" + } + }, + { + "name": "collapseBoxSetItems", + "in": "query", + "description": "Whether or not to hide items behind their boxsets.", + "schema": { + "type": "boolean" + } + }, + { + "name": "minWidth", + "in": "query", + "description": "Optional. Filter by the minimum width of the item.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minHeight", + "in": "query", + "description": "Optional. Filter by the minimum height of the item.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. Filter by the maximum width of the item.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. Filter by the maximum height of the item.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "is3D", + "in": "query", + "description": "Optional filter by items that are 3D, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "seriesStatus", + "in": "query", + "description": "Optional filter by Series Status. Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesStatus" + } + } + }, + { + "name": "nameStartsWithOrGreater", + "in": "query", + "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameStartsWith", + "in": "query", + "description": "Optional filter by items whose name is sorted equally than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameLessThan", + "in": "query", + "description": "Optional filter by items whose name is equally or lesser than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "studioIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "genreIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Optional. Enable the total record count.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional, include image information in output.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "Library" + ], + "summary": "Deletes items from the library and filesystem.", + "operationId": "DeleteItems", + "parameters": [ + { + "name": "ids", + "in": "query", + "description": "The item ids.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + ], + "responses": { + "204": { + "description": "Items deleted." + }, + "401": { + "description": "Unauthorized access.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/UserItems/{itemId}/UserData": { + "get": { + "tags": [ + "Items" + ], + "summary": "Get Item User Data.", + "operationId": "GetItemUserData", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "return item user data.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + } + } + }, + "404": { + "description": "Item is not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "Items" + ], + "summary": "Update Item User Data.", + "operationId": "UpdateItemUserData", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "New user data object.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateUserItemDataDto" + } + ], + "description": "This is used by the api to get information about a item user data." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateUserItemDataDto" + } + ], + "description": "This is used by the api to get information about a item user data." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateUserItemDataDto" + } + ], + "description": "This is used by the api to get information about a item user data." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "return updated user item data.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + } + } + }, + "404": { + "description": "Item is not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/UserItems/Resume": { + "get": { + "tags": [ + "Items" + ], + "summary": "Gets items based on a query.", + "operationId": "GetResumeItems", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "The start index.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "The item limit.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "searchTerm", + "in": "query", + "description": "The search term.", + "schema": { + "type": "string" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "mediaTypes", + "in": "query", + "description": "Optional. Filter by MediaType. Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "excludeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Optional. Enable the total record count.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "excludeActiveSessions", + "in": "query", + "description": "Optional. Whether to exclude the currently active sessions.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Items returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}": { + "post": { + "tags": [ + "ItemUpdate" + ], + "summary": "Updates an item.", + "operationId": "UpdateItem", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The new item properties.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemDto" + } + ], + "description": "This is strictly used as a data transfer object from the api layer.\r\nThis holds information about a BaseItem in a format that is convenient for the client." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemDto" + } + ], + "description": "This is strictly used as a data transfer object from the api layer.\r\nThis holds information about a BaseItem in a format that is convenient for the client." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemDto" + } + ], + "description": "This is strictly used as a data transfer object from the api layer.\r\nThis holds information about a BaseItem in a format that is convenient for the client." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Item updated." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "delete": { + "tags": [ + "Library" + ], + "summary": "Deletes an item from the library and filesystem.", + "operationId": "DeleteItem", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Item deleted." + }, + "401": { + "description": "Unauthorized access.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "get": { + "tags": [ + "UserLibrary" + ], + "summary": "Gets an item from a user's library.", + "operationId": "GetItem", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Item returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/ContentType": { + "post": { + "tags": [ + "ItemUpdate" + ], + "summary": "Updates an item's content type.", + "operationId": "UpdateItemContentType", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "contentType", + "in": "query", + "description": "The content type of the item.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Item content type updated." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Items/{itemId}/MetadataEditor": { + "get": { + "tags": [ + "ItemUpdate" + ], + "summary": "Gets metadata editor info for an item.", + "operationId": "GetMetadataEditorInfo", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Item metadata editor returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataEditorInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/MetadataEditorInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/MetadataEditorInfo" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Albums/{itemId}/Similar": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets similar items.", + "operationId": "GetSimilarAlbums", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "excludeArtistIds", + "in": "query", + "description": "Exclude artist ids.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + } + ], + "responses": { + "200": { + "description": "Similar items returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Artists/{itemId}/Similar": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets similar items.", + "operationId": "GetSimilarArtists", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "excludeArtistIds", + "in": "query", + "description": "Exclude artist ids.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + } + ], + "responses": { + "200": { + "description": "Similar items returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/Ancestors": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets all parents of an item.", + "operationId": "GetAncestors", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Item parents returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/CriticReviews": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets critic review for an item.", + "operationId": "GetCriticReviews", + "parameters": [ + { + "name": "itemId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Critic reviews returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/Download": { + "get": { + "tags": [ + "Library" + ], + "summary": "Downloads item media.", + "operationId": "GetDownload", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Media downloaded.", + "content": { + "video/*": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "Download", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/File": { + "get": { + "tags": [ + "Library" + ], + "summary": "Get the original file of an item.", + "operationId": "GetFile", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "File stream returned.", + "content": { + "video/*": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/Similar": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets similar items.", + "operationId": "GetSimilarItems", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "excludeArtistIds", + "in": "query", + "description": "Exclude artist ids.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + } + ], + "responses": { + "200": { + "description": "Similar items returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/ThemeMedia": { + "get": { + "tags": [ + "Library" + ], + "summary": "Get theme songs and videos for an item.", + "operationId": "GetThemeMedia", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "inheritFromParent", + "in": "query", + "description": "Optional. Determines whether or not parent items should be searched for theme media.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Optional. Sort Order - Ascending, Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + } + ], + "responses": { + "200": { + "description": "Theme songs and videos returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllThemeMediaResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/AllThemeMediaResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/AllThemeMediaResult" + } + } + } + }, + "404": { + "description": "Item not found." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/ThemeSongs": { + "get": { + "tags": [ + "Library" + ], + "summary": "Get theme songs for an item.", + "operationId": "GetThemeSongs", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "inheritFromParent", + "in": "query", + "description": "Optional. Determines whether or not parent items should be searched for theme media.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Optional. Sort Order - Ascending, Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + } + ], + "responses": { + "200": { + "description": "Theme songs returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThemeMediaResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ThemeMediaResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ThemeMediaResult" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/ThemeVideos": { + "get": { + "tags": [ + "Library" + ], + "summary": "Get theme videos for an item.", + "operationId": "GetThemeVideos", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "inheritFromParent", + "in": "query", + "description": "Optional. Determines whether or not parent items should be searched for theme media.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Optional. Sort Order - Ascending, Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + } + ], + "responses": { + "200": { + "description": "Theme videos returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThemeMediaResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ThemeMediaResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ThemeMediaResult" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/Counts": { + "get": { + "tags": [ + "Library" + ], + "summary": "Get item counts.", + "operationId": "GetItemCounts", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Optional. Get counts from a specific user's library.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional. Get counts of favorite items.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Item counts returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemCounts" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ItemCounts" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ItemCounts" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Libraries/AvailableOptions": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets the library options info.", + "operationId": "GetLibraryOptionsInfo", + "parameters": [ + { + "name": "libraryContentType", + "in": "query", + "description": "Library content type.", + "schema": { + "enum": [ + "unknown", + "movies", + "tvshows", + "music", + "musicvideos", + "trailers", + "homevideos", + "boxsets", + "books", + "photos", + "livetv", + "playlists", + "folders" + ], + "allOf": [ + { + "$ref": "#/components/schemas/CollectionType" + } + ] + } + }, + { + "name": "isNewLibrary", + "in": "query", + "description": "Whether this is a new library.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Library options info returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LibraryOptionsResultDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/LibraryOptionsResultDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/LibraryOptionsResultDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrDefault", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Library/Media/Updated": { + "post": { + "tags": [ + "Library" + ], + "summary": "Reports that new movies have been added by an external source.", + "operationId": "PostUpdatedMedia", + "requestBody": { + "description": "The update paths.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MediaUpdateInfoDto" + } + ], + "description": "Media Update Info Dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MediaUpdateInfoDto" + } + ], + "description": "Media Update Info Dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MediaUpdateInfoDto" + } + ], + "description": "Media Update Info Dto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Report success." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Library/MediaFolders": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets all user media folders.", + "operationId": "GetMediaFolders", + "parameters": [ + { + "name": "isHidden", + "in": "query", + "description": "Optional. Filter by folders that are marked hidden, or not.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Media folders returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Library/Movies/Added": { + "post": { + "tags": [ + "Library" + ], + "summary": "Reports that new movies have been added by an external source.", + "operationId": "PostAddedMovies", + "parameters": [ + { + "name": "tmdbId", + "in": "query", + "description": "The tmdbId.", + "schema": { + "type": "string" + } + }, + { + "name": "imdbId", + "in": "query", + "description": "The imdbId.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Report success." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Library/Movies/Updated": { + "post": { + "tags": [ + "Library" + ], + "summary": "Reports that new movies have been added by an external source.", + "operationId": "PostUpdatedMovies", + "parameters": [ + { + "name": "tmdbId", + "in": "query", + "description": "The tmdbId.", + "schema": { + "type": "string" + } + }, + { + "name": "imdbId", + "in": "query", + "description": "The imdbId.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Report success." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Library/PhysicalPaths": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets a list of physical paths from virtual folders.", + "operationId": "GetPhysicalPaths", + "responses": { + "200": { + "description": "Physical paths returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Library/Refresh": { + "post": { + "tags": [ + "Library" + ], + "summary": "Starts a library scan.", + "operationId": "RefreshLibrary", + "responses": { + "204": { + "description": "Library scan started." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Library/Series/Added": { + "post": { + "tags": [ + "Library" + ], + "summary": "Reports that new episodes of a series have been added by an external source.", + "operationId": "PostAddedSeries", + "parameters": [ + { + "name": "tvdbId", + "in": "query", + "description": "The tvdbId.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Report success." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Library/Series/Updated": { + "post": { + "tags": [ + "Library" + ], + "summary": "Reports that new episodes of a series have been added by an external source.", + "operationId": "PostUpdatedSeries", + "parameters": [ + { + "name": "tvdbId", + "in": "query", + "description": "The tvdbId.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Report success." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Movies/{itemId}/Similar": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets similar items.", + "operationId": "GetSimilarMovies", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "excludeArtistIds", + "in": "query", + "description": "Exclude artist ids.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + } + ], + "responses": { + "200": { + "description": "Similar items returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Shows/{itemId}/Similar": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets similar items.", + "operationId": "GetSimilarShows", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "excludeArtistIds", + "in": "query", + "description": "Exclude artist ids.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + } + ], + "responses": { + "200": { + "description": "Similar items returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Trailers/{itemId}/Similar": { + "get": { + "tags": [ + "Library" + ], + "summary": "Gets similar items.", + "operationId": "GetSimilarTrailers", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "excludeArtistIds", + "in": "query", + "description": "Exclude artist ids.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + } + ], + "responses": { + "200": { + "description": "Similar items returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Library/VirtualFolders": { + "get": { + "tags": [ + "LibraryStructure" + ], + "summary": "Gets all virtual folders.", + "operationId": "GetVirtualFolders", + "responses": { + "200": { + "description": "Virtual folders retrieved.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VirtualFolderInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VirtualFolderInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VirtualFolderInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "LibraryStructure" + ], + "summary": "Adds a virtual folder.", + "operationId": "AddVirtualFolder", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The name of the virtual folder.", + "schema": { + "type": "string" + } + }, + { + "name": "collectionType", + "in": "query", + "description": "The type of the collection.", + "schema": { + "enum": [ + "movies", + "tvshows", + "music", + "musicvideos", + "homevideos", + "boxsets", + "books", + "mixed" + ], + "allOf": [ + { + "$ref": "#/components/schemas/CollectionTypeOptions" + } + ] + } + }, + { + "name": "paths", + "in": "query", + "description": "The paths of the virtual folder.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "refreshLibrary", + "in": "query", + "description": "Whether to refresh the library.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "description": "The library options.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/AddVirtualFolderDto" + } + ], + "description": "Add virtual folder dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/AddVirtualFolderDto" + } + ], + "description": "Add virtual folder dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/AddVirtualFolderDto" + } + ], + "description": "Add virtual folder dto." + } + } + } + }, + "responses": { + "204": { + "description": "Folder added." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "LibraryStructure" + ], + "summary": "Removes a virtual folder.", + "operationId": "RemoveVirtualFolder", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The name of the folder.", + "schema": { + "type": "string" + } + }, + { + "name": "refreshLibrary", + "in": "query", + "description": "Whether to refresh the library.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "204": { + "description": "Folder removed." + }, + "404": { + "description": "Folder not found." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Library/VirtualFolders/LibraryOptions": { + "post": { + "tags": [ + "LibraryStructure" + ], + "summary": "Update library options.", + "operationId": "UpdateLibraryOptions", + "requestBody": { + "description": "The library name and options.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateLibraryOptionsDto" + } + ], + "description": "Update library options dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateLibraryOptionsDto" + } + ], + "description": "Update library options dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateLibraryOptionsDto" + } + ], + "description": "Update library options dto." + } + } + } + }, + "responses": { + "204": { + "description": "Library updated." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Library/VirtualFolders/Name": { + "post": { + "tags": [ + "LibraryStructure" + ], + "summary": "Renames a virtual folder.", + "operationId": "RenameVirtualFolder", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The name of the virtual folder.", + "schema": { + "type": "string" + } + }, + { + "name": "newName", + "in": "query", + "description": "The new name.", + "schema": { + "type": "string" + } + }, + { + "name": "refreshLibrary", + "in": "query", + "description": "Whether to refresh the library.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "204": { + "description": "Folder renamed." + }, + "404": { + "description": "Library doesn't exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Library already exists.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Library/VirtualFolders/Paths": { + "post": { + "tags": [ + "LibraryStructure" + ], + "summary": "Add a media path to a library.", + "operationId": "AddMediaPath", + "parameters": [ + { + "name": "refreshLibrary", + "in": "query", + "description": "Whether to refresh the library.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "description": "The media path dto.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MediaPathDto" + } + ], + "description": "Media Path dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MediaPathDto" + } + ], + "description": "Media Path dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MediaPathDto" + } + ], + "description": "Media Path dto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Media path added." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "LibraryStructure" + ], + "summary": "Remove a media path.", + "operationId": "RemoveMediaPath", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The name of the library.", + "schema": { + "type": "string" + } + }, + { + "name": "path", + "in": "query", + "description": "The path to remove.", + "schema": { + "type": "string" + } + }, + { + "name": "refreshLibrary", + "in": "query", + "description": "Whether to refresh the library.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "204": { + "description": "Media path removed." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Library/VirtualFolders/Paths/Update": { + "post": { + "tags": [ + "LibraryStructure" + ], + "summary": "Updates a media path.", + "operationId": "UpdateMediaPath", + "requestBody": { + "description": "The name of the library and path infos.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateMediaPathRequestDto" + } + ], + "description": "Update library options dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateMediaPathRequestDto" + } + ], + "description": "Update library options dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateMediaPathRequestDto" + } + ], + "description": "Update library options dto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Media path updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/ChannelMappingOptions": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Get channel mapping options.", + "operationId": "GetChannelMappingOptions", + "parameters": [ + { + "name": "providerId", + "in": "query", + "description": "Provider id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Channel mapping options returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChannelMappingOptionsDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ChannelMappingOptionsDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ChannelMappingOptionsDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/ChannelMappings": { + "post": { + "tags": [ + "LiveTv" + ], + "summary": "Set channel mappings.", + "operationId": "SetChannelMapping", + "requestBody": { + "description": "The set channel mapping dto.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetChannelMappingDto" + } + ], + "description": "Set channel mapping dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetChannelMappingDto" + } + ], + "description": "Set channel mapping dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetChannelMappingDto" + } + ], + "description": "Set channel mapping dto." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Created channel mapping returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TunerChannelMapping" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/TunerChannelMapping" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/TunerChannelMapping" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Channels": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets available live tv channels.", + "operationId": "GetLiveTvChannels", + "parameters": [ + { + "name": "type", + "in": "query", + "description": "Optional. Filter by channel type.", + "schema": { + "enum": [ + "TV", + "Radio" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelType" + } + ] + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "isMovie", + "in": "query", + "description": "Optional. Filter for movies.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSeries", + "in": "query", + "description": "Optional. Filter for series.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isNews", + "in": "query", + "description": "Optional. Filter for news.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isKids", + "in": "query", + "description": "Optional. Filter for kids.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSports", + "in": "query", + "description": "Optional. Filter for sports.", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional. Filter by channels that are favorites, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isLiked", + "in": "query", + "description": "Optional. Filter by channels that are liked, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isDisliked", + "in": "query", + "description": "Optional. Filter by channels that are disliked, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "\"Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Key to sort by.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Optional. Sort order.", + "schema": { + "enum": [ + "Ascending", + "Descending" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SortOrder" + } + ] + } + }, + { + "name": "enableFavoriteSorting", + "in": "query", + "description": "Optional. Incorporate favorite and like status into channel sorting.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "addCurrentProgram", + "in": "query", + "description": "Optional. Adds current program info to each channel.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Available live tv channels returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Channels/{channelId}": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets a live tv channel.", + "operationId": "GetChannel", + "parameters": [ + { + "name": "channelId", + "in": "path", + "description": "Channel id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Live tv channel returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/GuideInfo": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Get guide info.", + "operationId": "GetGuideInfo", + "responses": { + "200": { + "description": "Guide info returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuideInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/GuideInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/GuideInfo" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Info": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets available live tv services.", + "operationId": "GetLiveTvInfo", + "responses": { + "200": { + "description": "Available live tv services returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveTvInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/LiveTvInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/LiveTvInfo" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/ListingProviders": { + "post": { + "tags": [ + "LiveTv" + ], + "summary": "Adds a listings provider.", + "operationId": "AddListingProvider", + "parameters": [ + { + "name": "pw", + "in": "query", + "description": "Password.", + "schema": { + "type": "string" + } + }, + { + "name": "validateListings", + "in": "query", + "description": "Validate listings.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "validateLogin", + "in": "query", + "description": "Validate login.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "description": "New listings info.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ListingsProviderInfo" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ListingsProviderInfo" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ListingsProviderInfo" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Created listings provider returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListingsProviderInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ListingsProviderInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ListingsProviderInfo" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "LiveTv" + ], + "summary": "Delete listing provider.", + "operationId": "DeleteListingProvider", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "Listing provider id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Listing provider deleted." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/ListingProviders/Default": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets default listings provider info.", + "operationId": "GetDefaultListingProvider", + "responses": { + "200": { + "description": "Default listings provider info returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListingsProviderInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ListingsProviderInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ListingsProviderInfo" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/ListingProviders/Lineups": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets available lineups.", + "operationId": "GetLineups", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "Provider id.", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "description": "Provider type.", + "schema": { + "type": "string" + } + }, + { + "name": "location", + "in": "query", + "description": "Location.", + "schema": { + "type": "string" + } + }, + { + "name": "country", + "in": "query", + "description": "Country.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Available lineups returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/ListingProviders/SchedulesDirect/Countries": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets available countries.", + "operationId": "GetSchedulesDirectCountries", + "responses": { + "200": { + "description": "Available countries returned.", + "content": { + "application/json": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/LiveRecordings/{recordingId}/stream": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets a live tv recording stream.", + "operationId": "GetLiveRecordingFile", + "parameters": [ + { + "name": "recordingId", + "in": "path", + "description": "Recording id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Recording stream returned.", + "content": { + "video/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Recording not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/LiveTv/LiveStreamFiles/{streamId}/stream.{container}": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets a live tv channel stream.", + "operationId": "GetLiveStreamFile", + "parameters": [ + { + "name": "streamId", + "in": "path", + "description": "Stream id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "container", + "in": "path", + "description": "Container type.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Stream returned.", + "content": { + "video/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Stream not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/LiveTv/Programs": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets available live tv epgs.", + "operationId": "GetLiveTvPrograms", + "parameters": [ + { + "name": "channelIds", + "in": "query", + "description": "The channels to return guide information for.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "minStartDate", + "in": "query", + "description": "Optional. The minimum premiere start date.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "hasAired", + "in": "query", + "description": "Optional. Filter by programs that have completed airing, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isAiring", + "in": "query", + "description": "Optional. Filter by programs that are currently airing, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "maxStartDate", + "in": "query", + "description": "Optional. The maximum premiere start date.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "minEndDate", + "in": "query", + "description": "Optional. The minimum premiere end date.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "maxEndDate", + "in": "query", + "description": "Optional. The maximum premiere end date.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "isMovie", + "in": "query", + "description": "Optional. Filter for movies.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSeries", + "in": "query", + "description": "Optional. Filter for series.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isNews", + "in": "query", + "description": "Optional. Filter for news.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isKids", + "in": "query", + "description": "Optional. Filter for kids.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSports", + "in": "query", + "description": "Optional. Filter for sports.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited. Options: Name, StartDate.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort Order - Ascending,Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + }, + { + "name": "genres", + "in": "query", + "description": "The genres to return guide information for.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "genreIds", + "in": "query", + "description": "The genre ids to return guide information for.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "seriesTimerId", + "in": "query", + "description": "Optional. Filter by series timer id.", + "schema": { + "type": "string" + } + }, + { + "name": "librarySeriesId", + "in": "query", + "description": "Optional. Filter by library series id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Retrieve total record count.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Live tv epgs returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "LiveTv" + ], + "summary": "Gets available live tv epgs.", + "operationId": "GetPrograms", + "requestBody": { + "description": "Request body.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GetProgramsDto" + } + ], + "description": "Get programs dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GetProgramsDto" + } + ], + "description": "Get programs dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GetProgramsDto" + } + ], + "description": "Get programs dto." + } + } + } + }, + "responses": { + "200": { + "description": "Live tv epgs returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Programs/{programId}": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets a live tv program.", + "operationId": "GetProgram", + "parameters": [ + { + "name": "programId", + "in": "path", + "description": "Program id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Program returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Programs/Recommended": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets recommended live tv epgs.", + "operationId": "GetRecommendedPrograms", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Optional. filter by user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "isAiring", + "in": "query", + "description": "Optional. Filter by programs that are currently airing, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasAired", + "in": "query", + "description": "Optional. Filter by programs that have completed airing, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSeries", + "in": "query", + "description": "Optional. Filter for series.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMovie", + "in": "query", + "description": "Optional. Filter for movies.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isNews", + "in": "query", + "description": "Optional. Filter for news.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isKids", + "in": "query", + "description": "Optional. Filter for kids.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSports", + "in": "query", + "description": "Optional. Filter for sports.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "genreIds", + "in": "query", + "description": "The genres to return guide information for.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Retrieve total record count.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Recommended epgs returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Recordings": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets live tv recordings.", + "operationId": "GetRecordings", + "parameters": [ + { + "name": "channelId", + "in": "query", + "description": "Optional. Filter by channel id.", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "status", + "in": "query", + "description": "Optional. Filter by recording status.", + "schema": { + "enum": [ + "New", + "InProgress", + "Completed", + "Cancelled", + "ConflictedOk", + "ConflictedNotOk", + "Error" + ], + "allOf": [ + { + "$ref": "#/components/schemas/RecordingStatus" + } + ] + } + }, + { + "name": "isInProgress", + "in": "query", + "description": "Optional. Filter by recordings that are in progress, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "seriesTimerId", + "in": "query", + "description": "Optional. Filter by recordings belonging to a series timer.", + "schema": { + "type": "string" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMovie", + "in": "query", + "description": "Optional. Filter for movies.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSeries", + "in": "query", + "description": "Optional. Filter for series.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isKids", + "in": "query", + "description": "Optional. Filter for kids.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSports", + "in": "query", + "description": "Optional. Filter for sports.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isNews", + "in": "query", + "description": "Optional. Filter for news.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isLibraryItem", + "in": "query", + "description": "Optional. Filter for is library item.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Optional. Return total record count.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Live tv recordings returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Recordings/{recordingId}": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets a live tv recording.", + "operationId": "GetRecording", + "parameters": [ + { + "name": "recordingId", + "in": "path", + "description": "Recording id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Recording returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "LiveTv" + ], + "summary": "Deletes a live tv recording.", + "operationId": "DeleteRecording", + "parameters": [ + { + "name": "recordingId", + "in": "path", + "description": "Recording id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Recording deleted." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Recordings/Folders": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets recording folders.", + "operationId": "GetRecordingFolders", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Recording folders returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Recordings/Groups": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets live tv recording groups.", + "operationId": "GetRecordingGroups", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Recording groups returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Recordings/Groups/{groupId}": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Get recording group.", + "operationId": "GetRecordingGroup", + "parameters": [ + { + "name": "groupId", + "in": "path", + "description": "Group id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Recordings/Series": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets live tv recording series.", + "operationId": "GetRecordingsSeries", + "parameters": [ + { + "name": "channelId", + "in": "query", + "description": "Optional. Filter by channel id.", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "groupId", + "in": "query", + "description": "Optional. Filter by recording group.", + "schema": { + "type": "string" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "status", + "in": "query", + "description": "Optional. Filter by recording status.", + "schema": { + "enum": [ + "New", + "InProgress", + "Completed", + "Cancelled", + "ConflictedOk", + "ConflictedNotOk", + "Error" + ], + "allOf": [ + { + "$ref": "#/components/schemas/RecordingStatus" + } + ] + } + }, + { + "name": "isInProgress", + "in": "query", + "description": "Optional. Filter by recordings that are in progress, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "seriesTimerId", + "in": "query", + "description": "Optional. Filter by recordings belonging to a series timer.", + "schema": { + "type": "string" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Optional. Return total record count.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Live tv recordings returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/SeriesTimers": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets live tv series timers.", + "operationId": "GetSeriesTimers", + "parameters": [ + { + "name": "sortBy", + "in": "query", + "description": "Optional. Sort by SortName or Priority.", + "schema": { + "type": "string" + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Optional. Sort in Ascending or Descending order.", + "schema": { + "enum": [ + "Ascending", + "Descending" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SortOrder" + } + ] + } + } + ], + "responses": { + "200": { + "description": "Timers returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SeriesTimerInfoDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/SeriesTimerInfoDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/SeriesTimerInfoDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "LiveTv" + ], + "summary": "Creates a live tv series timer.", + "operationId": "CreateSeriesTimer", + "requestBody": { + "description": "New series timer info.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + ], + "description": "Class SeriesTimerInfoDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + ], + "description": "Class SeriesTimerInfoDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + ], + "description": "Class SeriesTimerInfoDto." + } + } + } + }, + "responses": { + "204": { + "description": "Series timer info created." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/SeriesTimers/{timerId}": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets a live tv series timer.", + "operationId": "GetSeriesTimer", + "parameters": [ + { + "name": "timerId", + "in": "path", + "description": "Timer id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Series timer returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + } + } + }, + "404": { + "description": "Series timer not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "LiveTv" + ], + "summary": "Cancels a live tv series timer.", + "operationId": "CancelSeriesTimer", + "parameters": [ + { + "name": "timerId", + "in": "path", + "description": "Timer id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Timer cancelled." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "LiveTv" + ], + "summary": "Updates a live tv series timer.", + "operationId": "UpdateSeriesTimer", + "parameters": [ + { + "name": "timerId", + "in": "path", + "description": "Timer id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "New series timer info.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + ], + "description": "Class SeriesTimerInfoDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + ], + "description": "Class SeriesTimerInfoDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + ], + "description": "Class SeriesTimerInfoDto." + } + } + } + }, + "responses": { + "204": { + "description": "Series timer updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Timers": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets the live tv timers.", + "operationId": "GetTimers", + "parameters": [ + { + "name": "channelId", + "in": "query", + "description": "Optional. Filter by channel id.", + "schema": { + "type": "string" + } + }, + { + "name": "seriesTimerId", + "in": "query", + "description": "Optional. Filter by timers belonging to a series timer.", + "schema": { + "type": "string" + } + }, + { + "name": "isActive", + "in": "query", + "description": "Optional. Filter by timers that are active.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isScheduled", + "in": "query", + "description": "Optional. Filter by timers that are scheduled.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimerInfoDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/TimerInfoDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/TimerInfoDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "LiveTv" + ], + "summary": "Creates a live tv timer.", + "operationId": "CreateTimer", + "requestBody": { + "description": "New timer info.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TimerInfoDto" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TimerInfoDto" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TimerInfoDto" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "Timer created." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Timers/{timerId}": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets a timer.", + "operationId": "GetTimer", + "parameters": [ + { + "name": "timerId", + "in": "path", + "description": "Timer id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Timer returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimerInfoDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/TimerInfoDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/TimerInfoDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "LiveTv" + ], + "summary": "Cancels a live tv timer.", + "operationId": "CancelTimer", + "parameters": [ + { + "name": "timerId", + "in": "path", + "description": "Timer id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Timer deleted." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "LiveTv" + ], + "summary": "Updates a live tv timer.", + "operationId": "UpdateTimer", + "parameters": [ + { + "name": "timerId", + "in": "path", + "description": "Timer id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "New timer info.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TimerInfoDto" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TimerInfoDto" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TimerInfoDto" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "Timer updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Timers/Defaults": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Gets the default values for a new timer.", + "operationId": "GetDefaultTimer", + "parameters": [ + { + "name": "programId", + "in": "query", + "description": "Optional. To attach default values based on a program.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Default values returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/TunerHosts": { + "post": { + "tags": [ + "LiveTv" + ], + "summary": "Adds a tuner host.", + "operationId": "AddTunerHost", + "requestBody": { + "description": "New tuner host.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TunerHostInfo" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TunerHostInfo" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/TunerHostInfo" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Created tuner host returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TunerHostInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/TunerHostInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/TunerHostInfo" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "LiveTv" + ], + "summary": "Deletes a tuner host.", + "operationId": "DeleteTunerHost", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "Tuner host id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Tuner host deleted." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/TunerHosts/Types": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Get tuner host types.", + "operationId": "GetTunerHostTypes", + "responses": { + "200": { + "description": "Tuner host types returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Tuners/{tunerId}/Reset": { + "post": { + "tags": [ + "LiveTv" + ], + "summary": "Resets a tv tuner.", + "operationId": "ResetTuner", + "parameters": [ + { + "name": "tunerId", + "in": "path", + "description": "Tuner id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Tuner reset." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Tuners/Discover": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Discover tuners.", + "operationId": "DiscoverTuners", + "parameters": [ + { + "name": "newDevicesOnly", + "in": "query", + "description": "Only discover new tuners.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Tuners returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TunerHostInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TunerHostInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TunerHostInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveTv/Tuners/Discvover": { + "get": { + "tags": [ + "LiveTv" + ], + "summary": "Discover tuners.", + "operationId": "DiscvoverTuners", + "parameters": [ + { + "name": "newDevicesOnly", + "in": "query", + "description": "Only discover new tuners.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Tuners returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TunerHostInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TunerHostInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TunerHostInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LiveTvManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Localization/Countries": { + "get": { + "tags": [ + "Localization" + ], + "summary": "Gets known countries.", + "operationId": "GetCountries", + "responses": { + "200": { + "description": "Known countries returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountryInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountryInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountryInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrDefault", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Localization/Cultures": { + "get": { + "tags": [ + "Localization" + ], + "summary": "Gets known cultures.", + "operationId": "GetCultures", + "responses": { + "200": { + "description": "Known cultures returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CultureDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CultureDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CultureDto" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrDefault", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Localization/Options": { + "get": { + "tags": [ + "Localization" + ], + "summary": "Gets localization options.", + "operationId": "GetLocalizationOptions", + "responses": { + "200": { + "description": "Localization options returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocalizationOption" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocalizationOption" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocalizationOption" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrDefault", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Localization/ParentalRatings": { + "get": { + "tags": [ + "Localization" + ], + "summary": "Gets known parental ratings.", + "operationId": "GetParentalRatings", + "responses": { + "200": { + "description": "Known parental ratings returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ParentalRating" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ParentalRating" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ParentalRating" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrDefault", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Audio/{itemId}/Lyrics": { + "get": { + "tags": [ + "Lyrics" + ], + "summary": "Gets an item's lyrics.", + "operationId": "GetLyrics", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Lyrics returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + } + } + }, + "404": { + "description": "Something went wrong. No Lyrics will be returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "Lyrics" + ], + "summary": "Upload an external lyric file.", + "operationId": "UploadLyrics", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item the lyric belongs to.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fileName", + "in": "query", + "description": "Name of the file being uploaded.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "200": { + "description": "Lyrics uploaded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + } + } + }, + "400": { + "description": "Error processing upload.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LyricManagement", + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "Lyrics" + ], + "summary": "Deletes an external lyric file.", + "operationId": "DeleteLyrics", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Lyric deleted." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LyricManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Audio/{itemId}/RemoteSearch/Lyrics": { + "get": { + "tags": [ + "Lyrics" + ], + "summary": "Search remote lyrics.", + "operationId": "SearchRemoteLyrics", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Lyrics retrieved.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLyricInfoDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLyricInfoDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLyricInfoDto" + } + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LyricManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Audio/{itemId}/RemoteSearch/Lyrics/{lyricId}": { + "post": { + "tags": [ + "Lyrics" + ], + "summary": "Downloads a remote lyric.", + "operationId": "DownloadRemoteLyrics", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "lyricId", + "in": "path", + "description": "The lyric id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Lyric downloaded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LyricManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Providers/Lyrics/{lyricId}": { + "get": { + "tags": [ + "Lyrics" + ], + "summary": "Gets the remote lyrics.", + "operationId": "GetRemoteLyrics", + "parameters": [ + { + "name": "lyricId", + "in": "path", + "description": "The remote provider item id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "File returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/LyricDto" + } + } + } + }, + "404": { + "description": "Lyric not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LyricManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/PlaybackInfo": { + "get": { + "tags": [ + "MediaInfo" + ], + "summary": "Gets live playback media info for an item.", + "operationId": "GetPlaybackInfo", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Playback info returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybackInfoResponse" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/PlaybackInfoResponse" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/PlaybackInfoResponse" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "MediaInfo" + ], + "summary": "Gets live playback media info for an item.", + "description": "For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.\r\nQuery parameters are obsolete.", + "operationId": "GetPostedPlaybackInfo", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "The user id.", + "deprecated": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "maxStreamingBitrate", + "in": "query", + "description": "The maximum streaming bitrate.", + "deprecated": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "The start time in ticks.", + "deprecated": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "The audio stream index.", + "deprecated": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "The subtitle stream index.", + "deprecated": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "The maximum number of audio channels.", + "deprecated": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media source id.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The livestream id.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "autoOpenLiveStream", + "in": "query", + "description": "Whether to auto open the livestream.", + "deprecated": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "enableDirectPlay", + "in": "query", + "description": "Whether to enable direct play. Default: true.", + "deprecated": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "enableDirectStream", + "in": "query", + "description": "Whether to enable direct stream. Default: true.", + "deprecated": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "enableTranscoding", + "in": "query", + "description": "Whether to enable transcoding. Default: true.", + "deprecated": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether to allow to copy the video stream. Default: true.", + "deprecated": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether to allow to copy the audio stream. Default: true.", + "deprecated": true, + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "description": "The playback info.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackInfoDto" + } + ], + "description": "Playback info dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackInfoDto" + } + ], + "description": "Playback info dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackInfoDto" + } + ], + "description": "Playback info dto." + } + } + } + }, + "responses": { + "200": { + "description": "Playback info returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybackInfoResponse" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/PlaybackInfoResponse" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/PlaybackInfoResponse" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveStreams/Close": { + "post": { + "tags": [ + "MediaInfo" + ], + "summary": "Closes a media source.", + "operationId": "CloseLiveStream", + "parameters": [ + { + "name": "liveStreamId", + "in": "query", + "description": "The livestream id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Livestream closed." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/LiveStreams/Open": { + "post": { + "tags": [ + "MediaInfo" + ], + "summary": "Opens a media source.", + "operationId": "OpenLiveStream", + "parameters": [ + { + "name": "openToken", + "in": "query", + "description": "The open token.", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "maxStreamingBitrate", + "in": "query", + "description": "The maximum streaming bitrate.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "The start time in ticks.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "The audio stream index.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "The subtitle stream index.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "The maximum number of audio channels.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "itemId", + "in": "query", + "description": "The item id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "enableDirectPlay", + "in": "query", + "description": "Whether to enable direct play. Default: true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableDirectStream", + "in": "query", + "description": "Whether to enable direct stream. Default: true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "alwaysBurnInSubtitleWhenTranscoding", + "in": "query", + "description": "Always burn-in subtitle when transcoding.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "description": "The open live stream dto.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/OpenLiveStreamDto" + } + ], + "description": "Open live stream dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/OpenLiveStreamDto" + } + ], + "description": "Open live stream dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/OpenLiveStreamDto" + } + ], + "description": "Open live stream dto." + } + } + } + }, + "responses": { + "200": { + "description": "Media source opened.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveStreamResponse" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/LiveStreamResponse" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/LiveStreamResponse" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Playback/BitrateTest": { + "get": { + "tags": [ + "MediaInfo" + ], + "summary": "Tests the network with a request with the size of the bitrate.", + "operationId": "GetBitrateTestBytes", + "parameters": [ + { + "name": "size", + "in": "query", + "description": "The bitrate. Defaults to 102400.", + "schema": { + "maximum": 100000000, + "minimum": 1, + "type": "integer", + "format": "int32", + "default": 102400 + } + } + ], + "responses": { + "200": { + "description": "Test buffer returned.", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/MediaSegments/{itemId}": { + "get": { + "tags": [ + "MediaSegments" + ], + "summary": "Gets all media segments based on an itemId.", + "operationId": "GetItemSegments", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The ItemId.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "includeSegmentTypes", + "in": "query", + "description": "Optional filter of requested segment types.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaSegmentType" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaSegmentDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/MediaSegmentDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/MediaSegmentDtoQueryResult" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Movies/Recommendations": { + "get": { + "tags": [ + "Movies" + ], + "summary": "Gets movie recommendations.", + "operationId": "GetMovieRecommendations", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. The fields to return.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "categoryLimit", + "in": "query", + "description": "The max number of categories to return.", + "schema": { + "type": "integer", + "format": "int32", + "default": 5 + } + }, + { + "name": "itemLimit", + "in": "query", + "description": "The max number of items to return per category.", + "schema": { + "type": "integer", + "format": "int32", + "default": 8 + } + } + ], + "responses": { + "200": { + "description": "Movie recommendations returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecommendationDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecommendationDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecommendationDto" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/MusicGenres": { + "get": { + "tags": [ + "MusicGenres" + ], + "summary": "Gets all music genres from a given item, folder, or the entire library.", + "operationId": "GetMusicGenres", + "parameters": [ + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "searchTerm", + "in": "query", + "description": "The search term.", + "schema": { + "type": "string" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "excludeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional filter by items that are marked as favorite, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional, the max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "nameStartsWithOrGreater", + "in": "query", + "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameStartsWith", + "in": "query", + "description": "Optional filter by items whose name is sorted equally than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameLessThan", + "in": "query", + "description": "Optional filter by items whose name is equally or lesser than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort Order - Ascending,Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional, include image information in output.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Optional. Include total record count.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Music genres returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/MusicGenres/{genreName}": { + "get": { + "tags": [ + "MusicGenres" + ], + "summary": "Gets a music genre, by name.", + "operationId": "GetMusicGenre", + "parameters": [ + { + "name": "genreName", + "in": "path", + "description": "The genre name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Packages": { + "get": { + "tags": [ + "Package" + ], + "summary": "Gets available packages.", + "operationId": "GetPackages", + "responses": { + "200": { + "description": "Available packages returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PackageInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PackageInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PackageInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Packages/{name}": { + "get": { + "tags": [ + "Package" + ], + "summary": "Gets a package by name or assembly GUID.", + "operationId": "GetPackageInfo", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "The name of the package.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "assemblyGuid", + "in": "query", + "description": "The GUID of the associated assembly.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Package retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PackageInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/PackageInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/PackageInfo" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Packages/Installed/{name}": { + "post": { + "tags": [ + "Package" + ], + "summary": "Installs a package.", + "operationId": "InstallPackage", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Package name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "assemblyGuid", + "in": "query", + "description": "GUID of the associated assembly.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "version", + "in": "query", + "description": "Optional version. Defaults to latest version.", + "schema": { + "type": "string" + } + }, + { + "name": "repositoryUrl", + "in": "query", + "description": "Optional. Specify the repository to install from.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Package found." + }, + "404": { + "description": "Package not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Packages/Installing/{packageId}": { + "delete": { + "tags": [ + "Package" + ], + "summary": "Cancels a package installation.", + "operationId": "CancelPackageInstallation", + "parameters": [ + { + "name": "packageId", + "in": "path", + "description": "Installation Id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Installation cancelled." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Repositories": { + "get": { + "tags": [ + "Package" + ], + "summary": "Gets all package repositories.", + "operationId": "GetRepositories", + "responses": { + "200": { + "description": "Package repositories returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RepositoryInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RepositoryInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RepositoryInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "post": { + "tags": [ + "Package" + ], + "summary": "Sets the enabled and existing package repositories.", + "operationId": "SetRepositories", + "requestBody": { + "description": "The list of package repositories.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RepositoryInfo" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RepositoryInfo" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RepositoryInfo" + } + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Package repositories saved." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Persons": { + "get": { + "tags": [ + "Persons" + ], + "summary": "Gets all persons.", + "operationId": "GetPersons", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "searchTerm", + "in": "query", + "description": "The search term.", + "schema": { + "type": "string" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "filters", + "in": "query", + "description": "Optional. Specify additional filters to apply.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFilter" + } + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional filter by items that are marked as favorite, or not. userId is required.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional, include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional, the max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "excludePersonTypes", + "in": "query", + "description": "Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "personTypes", + "in": "query", + "description": "Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "appearsInItemId", + "in": "query", + "description": "Optional. If specified, person results will be filtered on items related to said persons.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional, include image information in output.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Persons returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Persons/{name}": { + "get": { + "tags": [ + "Persons" + ], + "summary": "Get person by name.", + "operationId": "GetPerson", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Person name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Person returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "404": { + "description": "Person not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Playlists": { + "post": { + "tags": [ + "Playlists" + ], + "summary": "Creates a new playlist.", + "description": "For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.\r\nQuery parameters are obsolete.", + "operationId": "CreatePlaylist", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The playlist name.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "ids", + "in": "query", + "description": "The item ids.", + "deprecated": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "The user id.", + "deprecated": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "mediaType", + "in": "query", + "description": "The media type.", + "deprecated": true, + "schema": { + "enum": [ + "Unknown", + "Video", + "Audio", + "Photo", + "Book" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaType" + } + ] + } + } + ], + "requestBody": { + "description": "The create playlist payload.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/CreatePlaylistDto" + } + ], + "description": "Create new playlist dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/CreatePlaylistDto" + } + ], + "description": "Create new playlist dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/CreatePlaylistDto" + } + ], + "description": "Create new playlist dto." + } + } + } + }, + "responses": { + "200": { + "description": "Playlist created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaylistCreationResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/PlaylistCreationResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/PlaylistCreationResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Playlists/{playlistId}": { + "post": { + "tags": [ + "Playlists" + ], + "summary": "Updates a playlist.", + "operationId": "UpdatePlaylist", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The Jellyfin.Api.Models.PlaylistDtos.UpdatePlaylistDto id.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdatePlaylistDto" + } + ], + "description": "Update existing playlist dto. Fields set to `null` will not be updated and keep their current values." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdatePlaylistDto" + } + ], + "description": "Update existing playlist dto. Fields set to `null` will not be updated and keep their current values." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdatePlaylistDto" + } + ], + "description": "Update existing playlist dto. Fields set to `null` will not be updated and keep their current values." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Playlist updated." + }, + "403": { + "description": "Access forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Playlist not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "get": { + "tags": [ + "Playlists" + ], + "summary": "Get a playlist.", + "operationId": "GetPlaylist", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The playlist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaylistDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/PlaylistDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/PlaylistDto" + } + } + } + }, + "404": { + "description": "Playlist not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Playlists/{playlistId}/Items": { + "post": { + "tags": [ + "Playlists" + ], + "summary": "Adds items to a playlist.", + "operationId": "AddItemToPlaylist", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "ids", + "in": "query", + "description": "Item id, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "The userId.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Items added to playlist." + }, + "403": { + "description": "Access forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Playlist not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "Playlists" + ], + "summary": "Removes items from a playlist.", + "operationId": "RemoveItemFromPlaylist", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "entryIds", + "in": "query", + "description": "The item ids, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "204": { + "description": "Items removed." + }, + "403": { + "description": "Access forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Playlist not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "get": { + "tags": [ + "Playlists" + ], + "summary": "Gets the original items of a playlist.", + "operationId": "GetPlaylistItems", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + } + ], + "responses": { + "200": { + "description": "Original playlist returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Playlist not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Playlists/{playlistId}/Items/{itemId}/Move/{newIndex}": { + "post": { + "tags": [ + "Playlists" + ], + "summary": "Moves a playlist item.", + "operationId": "MoveItem", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "newIndex", + "in": "path", + "description": "The new index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "Item moved to new index." + }, + "403": { + "description": "Access forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Playlist not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Playlists/{playlistId}/Users": { + "get": { + "tags": [ + "Playlists" + ], + "summary": "Get a playlist's users.", + "operationId": "GetPlaylistUsers", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Found shares.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlaylistUserPermissions" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlaylistUserPermissions" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlaylistUserPermissions" + } + } + } + } + }, + "403": { + "description": "Access forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Playlist not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Playlists/{playlistId}/Users/{userId}": { + "get": { + "tags": [ + "Playlists" + ], + "summary": "Get a playlist user.", + "operationId": "GetPlaylistUser", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "path", + "description": "The user id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "User permission found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaylistUserPermissions" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/PlaylistUserPermissions" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/PlaylistUserPermissions" + } + } + } + }, + "403": { + "description": "Access forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Playlist not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "Playlists" + ], + "summary": "Modify a user of a playlist's users.", + "operationId": "UpdatePlaylistUser", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "path", + "description": "The user id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The Jellyfin.Api.Models.PlaylistDtos.UpdatePlaylistUserDto.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdatePlaylistUserDto" + } + ], + "description": "Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdatePlaylistUserDto" + } + ], + "description": "Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdatePlaylistUserDto" + } + ], + "description": "Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "User's permissions modified." + }, + "403": { + "description": "Access forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Playlist not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "Playlists" + ], + "summary": "Remove a user from a playlist's users.", + "operationId": "RemoveUserFromPlaylist", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The playlist id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "path", + "description": "The user id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "User permissions removed from playlist." + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "No playlist or user permissions found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "Unauthorized access." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/PlayingItems/{itemId}": { + "post": { + "tags": [ + "Playstate" + ], + "summary": "Reports that a session has begun playing an item.", + "operationId": "OnPlaybackStart", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The id of the MediaSource.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "The audio stream index.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "The subtitle stream index.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "playMethod", + "in": "query", + "description": "The play method.", + "schema": { + "enum": [ + "Transcode", + "DirectStream", + "DirectPlay" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlayMethod" + } + ] + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "canSeek", + "in": "query", + "description": "Indicates if the client can seek.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "204": { + "description": "Play start recorded." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "Playstate" + ], + "summary": "Reports that a session has stopped playing an item.", + "operationId": "OnPlaybackStopped", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The id of the MediaSource.", + "schema": { + "type": "string" + } + }, + { + "name": "nextMediaType", + "in": "query", + "description": "The next media type that will play.", + "schema": { + "type": "string" + } + }, + { + "name": "positionTicks", + "in": "query", + "description": "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Playback stop recorded." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/PlayingItems/{itemId}/Progress": { + "post": { + "tags": [ + "Playstate" + ], + "summary": "Reports a session's playback progress.", + "operationId": "OnPlaybackProgress", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The id of the MediaSource.", + "schema": { + "type": "string" + } + }, + { + "name": "positionTicks", + "in": "query", + "description": "Optional. The current position, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "The audio stream index.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "The subtitle stream index.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "volumeLevel", + "in": "query", + "description": "Scale of 0-100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "playMethod", + "in": "query", + "description": "The play method.", + "schema": { + "enum": [ + "Transcode", + "DirectStream", + "DirectPlay" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlayMethod" + } + ] + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "repeatMode", + "in": "query", + "description": "The repeat mode.", + "schema": { + "enum": [ + "RepeatNone", + "RepeatAll", + "RepeatOne" + ], + "allOf": [ + { + "$ref": "#/components/schemas/RepeatMode" + } + ] + } + }, + { + "name": "isPaused", + "in": "query", + "description": "Indicates if the player is paused.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "isMuted", + "in": "query", + "description": "Indicates if the player is muted.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "204": { + "description": "Play progress recorded." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/Playing": { + "post": { + "tags": [ + "Playstate" + ], + "summary": "Reports playback has started within a session.", + "operationId": "ReportPlaybackStart", + "requestBody": { + "description": "The playback start info.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackStartInfo" + } + ], + "description": "Class PlaybackStartInfo." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackStartInfo" + } + ], + "description": "Class PlaybackStartInfo." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackStartInfo" + } + ], + "description": "Class PlaybackStartInfo." + } + } + } + }, + "responses": { + "204": { + "description": "Playback start recorded." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/Playing/Ping": { + "post": { + "tags": [ + "Playstate" + ], + "summary": "Pings a playback session.", + "operationId": "PingPlaybackSession", + "parameters": [ + { + "name": "playSessionId", + "in": "query", + "description": "Playback session id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Playback session pinged." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/Playing/Progress": { + "post": { + "tags": [ + "Playstate" + ], + "summary": "Reports playback progress within a session.", + "operationId": "ReportPlaybackProgress", + "requestBody": { + "description": "The playback progress info.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackProgressInfo" + } + ], + "description": "Class PlaybackProgressInfo." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackProgressInfo" + } + ], + "description": "Class PlaybackProgressInfo." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackProgressInfo" + } + ], + "description": "Class PlaybackProgressInfo." + } + } + } + }, + "responses": { + "204": { + "description": "Playback progress recorded." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/Playing/Stopped": { + "post": { + "tags": [ + "Playstate" + ], + "summary": "Reports playback has stopped within a session.", + "operationId": "ReportPlaybackStopped", + "requestBody": { + "description": "The playback stop info.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackStopInfo" + } + ], + "description": "Class PlaybackStopInfo." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackStopInfo" + } + ], + "description": "Class PlaybackStopInfo." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackStopInfo" + } + ], + "description": "Class PlaybackStopInfo." + } + } + } + }, + "responses": { + "204": { + "description": "Playback stop recorded." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/UserPlayedItems/{itemId}": { + "post": { + "tags": [ + "Playstate" + ], + "summary": "Marks an item as played for user.", + "operationId": "MarkPlayedItem", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "datePlayed", + "in": "query", + "description": "Optional. The date the item was played.", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "Item marked as played.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "Playstate" + ], + "summary": "Marks an item as unplayed for user.", + "operationId": "MarkUnplayedItem", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Item marked as unplayed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Plugins": { + "get": { + "tags": [ + "Plugins" + ], + "summary": "Gets a list of currently installed plugins.", + "operationId": "GetPlugins", + "responses": { + "200": { + "description": "Installed plugins returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PluginInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PluginInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PluginInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Plugins/{pluginId}": { + "delete": { + "tags": [ + "Plugins" + ], + "summary": "Uninstalls a plugin.", + "operationId": "UninstallPlugin", + "parameters": [ + { + "name": "pluginId", + "in": "path", + "description": "Plugin id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Plugin uninstalled." + }, + "404": { + "description": "Plugin not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Plugins/{pluginId}/{version}": { + "delete": { + "tags": [ + "Plugins" + ], + "summary": "Uninstalls a plugin by version.", + "operationId": "UninstallPluginByVersion", + "parameters": [ + { + "name": "pluginId", + "in": "path", + "description": "Plugin id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "version", + "in": "path", + "description": "Plugin version.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Plugin uninstalled." + }, + "404": { + "description": "Plugin not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Plugins/{pluginId}/{version}/Disable": { + "post": { + "tags": [ + "Plugins" + ], + "summary": "Disable a plugin.", + "operationId": "DisablePlugin", + "parameters": [ + { + "name": "pluginId", + "in": "path", + "description": "Plugin id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "version", + "in": "path", + "description": "Plugin version.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Plugin disabled." + }, + "404": { + "description": "Plugin not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Plugins/{pluginId}/{version}/Enable": { + "post": { + "tags": [ + "Plugins" + ], + "summary": "Enables a disabled plugin.", + "operationId": "EnablePlugin", + "parameters": [ + { + "name": "pluginId", + "in": "path", + "description": "Plugin id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "version", + "in": "path", + "description": "Plugin version.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Plugin enabled." + }, + "404": { + "description": "Plugin not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Plugins/{pluginId}/{version}/Image": { + "get": { + "tags": [ + "Plugins" + ], + "summary": "Gets a plugin's image.", + "operationId": "GetPluginImage", + "parameters": [ + { + "name": "pluginId", + "in": "path", + "description": "Plugin id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "version", + "in": "path", + "description": "Plugin version.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Plugin image returned.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Plugins/{pluginId}/Configuration": { + "get": { + "tags": [ + "Plugins" + ], + "summary": "Gets plugin configuration.", + "operationId": "GetPluginConfiguration", + "parameters": [ + { + "name": "pluginId", + "in": "path", + "description": "Plugin id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Plugin configuration returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BasePluginConfiguration" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BasePluginConfiguration" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BasePluginConfiguration" + } + } + } + }, + "404": { + "description": "Plugin not found or plugin configuration not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "post": { + "tags": [ + "Plugins" + ], + "summary": "Updates plugin configuration.", + "description": "Accepts plugin configuration as JSON body.", + "operationId": "UpdatePluginConfiguration", + "parameters": [ + { + "name": "pluginId", + "in": "path", + "description": "Plugin id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Plugin configuration updated." + }, + "404": { + "description": "Plugin not found or plugin does not have configuration.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Plugins/{pluginId}/Manifest": { + "post": { + "tags": [ + "Plugins" + ], + "summary": "Gets a plugin's manifest.", + "operationId": "GetPluginManifest", + "parameters": [ + { + "name": "pluginId", + "in": "path", + "description": "Plugin id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Plugin manifest returned." + }, + "404": { + "description": "Plugin not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/QuickConnect/Authorize": { + "post": { + "tags": [ + "QuickConnect" + ], + "summary": "Authorizes a pending quick connect request.", + "operationId": "AuthorizeQuickConnect", + "parameters": [ + { + "name": "code", + "in": "query", + "description": "Quick connect code to authorize.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "The user the authorize. Access to the requested user is required.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Quick connect result authorized successfully.", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "boolean" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "boolean" + } + } + } + }, + "403": { + "description": "Unknown user id.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/QuickConnect/Connect": { + "get": { + "tags": [ + "QuickConnect" + ], + "summary": "Attempts to retrieve authentication information.", + "operationId": "GetQuickConnectState", + "parameters": [ + { + "name": "secret", + "in": "query", + "description": "Secret previously returned from the Initiate endpoint.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Quick connect result returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuickConnectResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/QuickConnectResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/QuickConnectResult" + } + } + } + }, + "404": { + "description": "Unknown quick connect secret.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/QuickConnect/Enabled": { + "get": { + "tags": [ + "QuickConnect" + ], + "summary": "Gets the current quick connect state.", + "operationId": "GetQuickConnectEnabled", + "responses": { + "200": { + "description": "Quick connect state returned.", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "boolean" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "boolean" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/QuickConnect/Initiate": { + "post": { + "tags": [ + "QuickConnect" + ], + "summary": "Initiate a new quick connect request.", + "operationId": "InitiateQuickConnect", + "responses": { + "200": { + "description": "Quick connect request successfully created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuickConnectResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/QuickConnectResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/QuickConnectResult" + } + } + } + }, + "401": { + "description": "Quick connect is not active on this server." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Items/{itemId}/RemoteImages": { + "get": { + "tags": [ + "RemoteImage" + ], + "summary": "Gets available remote images for an item.", + "operationId": "GetRemoteImages", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item Id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "type", + "in": "query", + "description": "The image type.", + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ] + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "providerName", + "in": "query", + "description": "Optional. The image provider to use.", + "schema": { + "type": "string" + } + }, + { + "name": "includeAllLanguages", + "in": "query", + "description": "Optional. Include all languages.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Remote Images returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoteImageResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/RemoteImageResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/RemoteImageResult" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/RemoteImages/Download": { + "post": { + "tags": [ + "RemoteImage" + ], + "summary": "Downloads a remote image for an item.", + "operationId": "DownloadRemoteImage", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item Id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "type", + "in": "query", + "description": "The image type.", + "required": true, + "schema": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Enum ImageType." + } + }, + { + "name": "imageUrl", + "in": "query", + "description": "The image url.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Remote image downloaded." + }, + "404": { + "description": "Remote image not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Items/{itemId}/RemoteImages/Providers": { + "get": { + "tags": [ + "RemoteImage" + ], + "summary": "Gets available remote image providers for an item.", + "operationId": "GetRemoteImageProviders", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "Item Id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Returned remote image providers.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageProviderInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageProviderInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageProviderInfo" + } + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/ScheduledTasks": { + "get": { + "tags": [ + "ScheduledTasks" + ], + "summary": "Get tasks.", + "operationId": "GetTasks", + "parameters": [ + { + "name": "isHidden", + "in": "query", + "description": "Optional filter tasks that are hidden, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isEnabled", + "in": "query", + "description": "Optional filter tasks that are enabled, or not.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Scheduled tasks retrieved.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskInfo" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/ScheduledTasks/{taskId}": { + "get": { + "tags": [ + "ScheduledTasks" + ], + "summary": "Get task by id.", + "operationId": "GetTask", + "parameters": [ + { + "name": "taskId", + "in": "path", + "description": "Task Id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Task retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/TaskInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/TaskInfo" + } + } + } + }, + "404": { + "description": "Task not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/ScheduledTasks/{taskId}/Triggers": { + "post": { + "tags": [ + "ScheduledTasks" + ], + "summary": "Update specified task triggers.", + "operationId": "UpdateTask", + "parameters": [ + { + "name": "taskId", + "in": "path", + "description": "Task Id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Triggers.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskTriggerInfo" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskTriggerInfo" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskTriggerInfo" + } + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Task triggers updated." + }, + "404": { + "description": "Task not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/ScheduledTasks/Running/{taskId}": { + "post": { + "tags": [ + "ScheduledTasks" + ], + "summary": "Start specified task.", + "operationId": "StartTask", + "parameters": [ + { + "name": "taskId", + "in": "path", + "description": "Task Id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Task started." + }, + "404": { + "description": "Task not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + }, + "delete": { + "tags": [ + "ScheduledTasks" + ], + "summary": "Stop specified task.", + "operationId": "StopTask", + "parameters": [ + { + "name": "taskId", + "in": "path", + "description": "Task Id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Task stopped." + }, + "404": { + "description": "Task not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Search/Hints": { + "get": { + "tags": [ + "Search" + ], + "summary": "Gets the search hint result.", + "operationId": "GetSearchHints", + "parameters": [ + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Supply a user id to search within a user's library or omit to search all.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "searchTerm", + "in": "query", + "description": "The search term to filter on.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "If specified, only results with the specified item types are returned. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "excludeItemTypes", + "in": "query", + "description": "If specified, results with these item types are filtered out. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "mediaTypes", + "in": "query", + "description": "If specified, only results with the specified media types are returned. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + } + } + }, + { + "name": "parentId", + "in": "query", + "description": "If specified, only children of the parent are returned.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "isMovie", + "in": "query", + "description": "Optional filter for movies.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSeries", + "in": "query", + "description": "Optional filter for series.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isNews", + "in": "query", + "description": "Optional filter for news.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isKids", + "in": "query", + "description": "Optional filter for kids.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSports", + "in": "query", + "description": "Optional filter for sports.", + "schema": { + "type": "boolean" + } + }, + { + "name": "includePeople", + "in": "query", + "description": "Optional filter whether to include people.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "includeMedia", + "in": "query", + "description": "Optional filter whether to include media.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "includeGenres", + "in": "query", + "description": "Optional filter whether to include genres.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "includeStudios", + "in": "query", + "description": "Optional filter whether to include studios.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "includeArtists", + "in": "query", + "description": "Optional filter whether to include artists.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Search hint returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchHintResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/SearchHintResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/SearchHintResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Auth/PasswordResetProviders": { + "get": { + "tags": [ + "Session" + ], + "summary": "Get all password reset providers.", + "operationId": "GetPasswordResetProviders", + "responses": { + "200": { + "description": "Password reset providers retrieved.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Auth/Providers": { + "get": { + "tags": [ + "Session" + ], + "summary": "Get all auth providers.", + "operationId": "GetAuthProviders", + "responses": { + "200": { + "description": "Auth providers retrieved.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Sessions": { + "get": { + "tags": [ + "Session" + ], + "summary": "Gets a list of sessions.", + "operationId": "GetSessions", + "parameters": [ + { + "name": "controllableByUserId", + "in": "query", + "description": "Filter by sessions that a given user is allowed to remote control.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "Filter by device Id.", + "schema": { + "type": "string" + } + }, + { + "name": "activeWithinSeconds", + "in": "query", + "description": "Optional. Filter by sessions that were active in the last n seconds.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "List of sessions returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionInfoDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionInfoDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionInfoDto" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/{sessionId}/Command": { + "post": { + "tags": [ + "Session" + ], + "summary": "Issues a full general command to a client.", + "operationId": "SendFullGeneralCommand", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The session id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The MediaBrowser.Model.Session.GeneralCommand.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GeneralCommand" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GeneralCommand" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GeneralCommand" + } + ] + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Full general command sent to session." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/{sessionId}/Command/{command}": { + "post": { + "tags": [ + "Session" + ], + "summary": "Issues a general command to a client.", + "operationId": "SendGeneralCommand", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The session id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "command", + "in": "path", + "description": "The command to send.", + "required": true, + "schema": { + "enum": [ + "MoveUp", + "MoveDown", + "MoveLeft", + "MoveRight", + "PageUp", + "PageDown", + "PreviousLetter", + "NextLetter", + "ToggleOsd", + "ToggleContextMenu", + "Select", + "Back", + "TakeScreenshot", + "SendKey", + "SendString", + "GoHome", + "GoToSettings", + "VolumeUp", + "VolumeDown", + "Mute", + "Unmute", + "ToggleMute", + "SetVolume", + "SetAudioStreamIndex", + "SetSubtitleStreamIndex", + "ToggleFullscreen", + "DisplayContent", + "GoToSearch", + "DisplayMessage", + "SetRepeatMode", + "ChannelUp", + "ChannelDown", + "Guide", + "ToggleStats", + "PlayMediaSource", + "PlayTrailers", + "SetShuffleQueue", + "PlayState", + "PlayNext", + "ToggleOsdMenu", + "Play", + "SetMaxStreamingBitrate", + "SetPlaybackOrder" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GeneralCommandType" + } + ], + "description": "This exists simply to identify a set of known commands." + } + } + ], + "responses": { + "204": { + "description": "General command sent to session." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/{sessionId}/Message": { + "post": { + "tags": [ + "Session" + ], + "summary": "Issues a command to a client to display a message to the user.", + "operationId": "SendMessageCommand", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The session id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The MediaBrowser.Model.Session.MessageCommand object containing Header, Message Text, and TimeoutMs.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MessageCommand" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MessageCommand" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MessageCommand" + } + ] + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Message sent." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/{sessionId}/Playing": { + "post": { + "tags": [ + "Session" + ], + "summary": "Instructs a session to play an item.", + "operationId": "Play", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The session id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "playCommand", + "in": "query", + "description": "The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.", + "required": true, + "schema": { + "enum": [ + "PlayNow", + "PlayNext", + "PlayLast", + "PlayInstantMix", + "PlayShuffle" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlayCommand" + } + ], + "description": "Enum PlayCommand." + } + }, + { + "name": "itemIds", + "in": "query", + "description": "The ids of the items to play, comma delimited.", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "startPositionTicks", + "in": "query", + "description": "The starting position of the first item.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "Optional. The media source id.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to play.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to play.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The start index.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "Instruction sent to session." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/{sessionId}/Playing/{command}": { + "post": { + "tags": [ + "Session" + ], + "summary": "Issues a playstate command to a client.", + "operationId": "SendPlaystateCommand", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The session id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "command", + "in": "path", + "description": "The MediaBrowser.Model.Session.PlaystateCommand.", + "required": true, + "schema": { + "enum": [ + "Stop", + "Pause", + "Unpause", + "NextTrack", + "PreviousTrack", + "Seek", + "Rewind", + "FastForward", + "PlayPause" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlaystateCommand" + } + ], + "description": "Enum PlaystateCommand." + } + }, + { + "name": "seekPositionTicks", + "in": "query", + "description": "The optional position ticks.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "controllingUserId", + "in": "query", + "description": "The optional controlling user id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Playstate command sent to session." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/{sessionId}/System/{command}": { + "post": { + "tags": [ + "Session" + ], + "summary": "Issues a system command to a client.", + "operationId": "SendSystemCommand", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The session id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "command", + "in": "path", + "description": "The command to send.", + "required": true, + "schema": { + "enum": [ + "MoveUp", + "MoveDown", + "MoveLeft", + "MoveRight", + "PageUp", + "PageDown", + "PreviousLetter", + "NextLetter", + "ToggleOsd", + "ToggleContextMenu", + "Select", + "Back", + "TakeScreenshot", + "SendKey", + "SendString", + "GoHome", + "GoToSettings", + "VolumeUp", + "VolumeDown", + "Mute", + "Unmute", + "ToggleMute", + "SetVolume", + "SetAudioStreamIndex", + "SetSubtitleStreamIndex", + "ToggleFullscreen", + "DisplayContent", + "GoToSearch", + "DisplayMessage", + "SetRepeatMode", + "ChannelUp", + "ChannelDown", + "Guide", + "ToggleStats", + "PlayMediaSource", + "PlayTrailers", + "SetShuffleQueue", + "PlayState", + "PlayNext", + "ToggleOsdMenu", + "Play", + "SetMaxStreamingBitrate", + "SetPlaybackOrder" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GeneralCommandType" + } + ], + "description": "This exists simply to identify a set of known commands." + } + } + ], + "responses": { + "204": { + "description": "System command sent to session." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/{sessionId}/User/{userId}": { + "post": { + "tags": [ + "Session" + ], + "summary": "Adds an additional user to a session.", + "operationId": "AddUserToSession", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The session id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "path", + "description": "The user id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "User added to session." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "Session" + ], + "summary": "Removes an additional user from a session.", + "operationId": "RemoveUserFromSession", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The session id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "path", + "description": "The user id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "User removed from session." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/{sessionId}/Viewing": { + "post": { + "tags": [ + "Session" + ], + "summary": "Instructs a session to browse to an item or view.", + "operationId": "DisplayContent", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The session Id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "itemType", + "in": "query", + "description": "The type of item to browse to.", + "required": true, + "schema": { + "enum": [ + "AggregateFolder", + "Audio", + "AudioBook", + "BasePluginFolder", + "Book", + "BoxSet", + "Channel", + "ChannelFolderItem", + "CollectionFolder", + "Episode", + "Folder", + "Genre", + "ManualPlaylistsFolder", + "Movie", + "LiveTvChannel", + "LiveTvProgram", + "MusicAlbum", + "MusicArtist", + "MusicGenre", + "MusicVideo", + "Person", + "Photo", + "PhotoAlbum", + "Playlist", + "PlaylistsFolder", + "Program", + "Recording", + "Season", + "Series", + "Studio", + "Trailer", + "TvChannel", + "TvProgram", + "UserRootFolder", + "UserView", + "Video", + "Year" + ], + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemKind" + } + ], + "description": "The base item kind." + } + }, + { + "name": "itemId", + "in": "query", + "description": "The Id of the item.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "itemName", + "in": "query", + "description": "The name of the item.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Instruction sent to session." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/Capabilities": { + "post": { + "tags": [ + "Session" + ], + "summary": "Updates capabilities for a device.", + "operationId": "PostCapabilities", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "The session id.", + "schema": { + "type": "string" + } + }, + { + "name": "playableMediaTypes", + "in": "query", + "description": "A list of playable media types, comma delimited. Audio, Video, Book, Photo.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + } + } + }, + { + "name": "supportedCommands", + "in": "query", + "description": "A list of supported remote control commands, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GeneralCommandType" + } + } + }, + { + "name": "supportsMediaControl", + "in": "query", + "description": "Determines whether media can be played remotely..", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "supportsPersistentIdentifier", + "in": "query", + "description": "Determines whether the device supports a unique identifier.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "204": { + "description": "Capabilities posted." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/Capabilities/Full": { + "post": { + "tags": [ + "Session" + ], + "summary": "Updates capabilities for a device.", + "operationId": "PostFullCapabilities", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "The session id.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The MediaBrowser.Model.Session.ClientCapabilities.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ClientCapabilitiesDto" + } + ], + "description": "Client capabilities dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ClientCapabilitiesDto" + } + ], + "description": "Client capabilities dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ClientCapabilitiesDto" + } + ], + "description": "Client capabilities dto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Capabilities updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/Logout": { + "post": { + "tags": [ + "Session" + ], + "summary": "Reports that a session has ended.", + "operationId": "ReportSessionEnded", + "responses": { + "204": { + "description": "Session end reported to server." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Sessions/Viewing": { + "post": { + "tags": [ + "Session" + ], + "summary": "Reports that a session is viewing an item.", + "operationId": "ReportViewing", + "parameters": [ + { + "name": "sessionId", + "in": "query", + "description": "The session id.", + "schema": { + "type": "string" + } + }, + { + "name": "itemId", + "in": "query", + "description": "The item id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Session reported to server." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Startup/Complete": { + "post": { + "tags": [ + "Startup" + ], + "summary": "Completes the startup wizard.", + "operationId": "CompleteWizard", + "responses": { + "204": { + "description": "Startup wizard completed." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Startup/Configuration": { + "get": { + "tags": [ + "Startup" + ], + "summary": "Gets the initial startup wizard configuration.", + "operationId": "GetStartupConfiguration", + "responses": { + "200": { + "description": "Initial startup wizard configuration retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartupConfigurationDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/StartupConfigurationDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/StartupConfigurationDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "Startup" + ], + "summary": "Sets the initial startup wizard configuration.", + "operationId": "UpdateInitialConfiguration", + "requestBody": { + "description": "The updated startup configuration.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/StartupConfigurationDto" + } + ], + "description": "The startup configuration DTO." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/StartupConfigurationDto" + } + ], + "description": "The startup configuration DTO." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/StartupConfigurationDto" + } + ], + "description": "The startup configuration DTO." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Configuration saved." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Startup/FirstUser": { + "get": { + "tags": [ + "Startup" + ], + "summary": "Gets the first user.", + "operationId": "GetFirstUser_2", + "responses": { + "200": { + "description": "Initial user retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartupUserDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/StartupUserDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/StartupUserDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Startup/RemoteAccess": { + "post": { + "tags": [ + "Startup" + ], + "summary": "Sets remote access and UPnP.", + "operationId": "SetRemoteAccess", + "requestBody": { + "description": "The startup remote access dto.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/StartupRemoteAccessDto" + } + ], + "description": "Startup remote access dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/StartupRemoteAccessDto" + } + ], + "description": "Startup remote access dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/StartupRemoteAccessDto" + } + ], + "description": "Startup remote access dto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Configuration saved." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Startup/User": { + "get": { + "tags": [ + "Startup" + ], + "summary": "Gets the first user.", + "operationId": "GetFirstUser", + "responses": { + "200": { + "description": "Initial user retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartupUserDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/StartupUserDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/StartupUserDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "Startup" + ], + "summary": "Sets the user name and password.", + "operationId": "UpdateStartupUser", + "requestBody": { + "description": "The DTO containing username and password.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/StartupUserDto" + } + ], + "description": "The startup user DTO." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/StartupUserDto" + } + ], + "description": "The startup user DTO." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/StartupUserDto" + } + ], + "description": "The startup user DTO." + } + } + } + }, + "responses": { + "204": { + "description": "Updated user name and password." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrElevated", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Studios": { + "get": { + "tags": [ + "Studios" + ], + "summary": "Gets all studios from a given item, folder, or the entire library.", + "operationId": "GetStudios", + "parameters": [ + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "searchTerm", + "in": "query", + "description": "Optional. Search term.", + "schema": { + "type": "string" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "excludeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional filter by items that are marked as favorite, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional, include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional, the max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "nameStartsWithOrGreater", + "in": "query", + "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameStartsWith", + "in": "query", + "description": "Optional filter by items whose name is sorted equally than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameLessThan", + "in": "query", + "description": "Optional filter by items whose name is equally or lesser than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional, include image information in output.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Total record count.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Studios returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Studios/{name}": { + "get": { + "tags": [ + "Studios" + ], + "summary": "Gets a studio by name.", + "operationId": "GetStudio", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Studio name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Studio returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/FallbackFont/Fonts": { + "get": { + "tags": [ + "Subtitle" + ], + "summary": "Gets a list of available fallback font files.", + "operationId": "GetFallbackFontList", + "responses": { + "200": { + "description": "Information retrieved.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FontFile" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FontFile" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FontFile" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/FallbackFont/Fonts/{name}": { + "get": { + "tags": [ + "Subtitle" + ], + "summary": "Gets a fallback font file.", + "operationId": "GetFallbackFont", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "The name of the fallback font file to get.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Fallback font file retrieved.", + "content": { + "font/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/RemoteSearch/Subtitles/{language}": { + "get": { + "tags": [ + "Subtitle" + ], + "summary": "Search remote subtitles.", + "operationId": "SearchRemoteSubtitles", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "language", + "in": "path", + "description": "The language of the subtitles.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "isPerfectMatch", + "in": "query", + "description": "Optional. Only show subtitles which are a perfect match.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Subtitles retrieved.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSubtitleInfo" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSubtitleInfo" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSubtitleInfo" + } + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SubtitleManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}": { + "post": { + "tags": [ + "Subtitle" + ], + "summary": "Downloads a remote subtitle.", + "operationId": "DownloadRemoteSubtitles", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "subtitleId", + "in": "path", + "description": "The subtitle id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Subtitle downloaded." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SubtitleManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Providers/Subtitles/Subtitles/{subtitleId}": { + "get": { + "tags": [ + "Subtitle" + ], + "summary": "Gets the remote subtitles.", + "operationId": "GetRemoteSubtitles", + "parameters": [ + { + "name": "subtitleId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "File returned.", + "content": { + "text/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SubtitleManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8": { + "get": { + "tags": [ + "Subtitle" + ], + "summary": "Gets an HLS subtitle playlist.", + "operationId": "GetSubtitlePlaylist", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "index", + "in": "path", + "description": "The subtitle stream index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "path", + "description": "The media source id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The subtitle segment length.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Subtitle playlist retrieved.", + "content": { + "application/x-mpegURL": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{itemId}/Subtitles": { + "post": { + "tags": [ + "Subtitle" + ], + "summary": "Upload an external subtitle file.", + "operationId": "UploadSubtitle", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item the subtitle belongs to.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The request body.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UploadSubtitleDto" + } + ], + "description": "Upload subtitles dto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UploadSubtitleDto" + } + ], + "description": "Upload subtitles dto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UploadSubtitleDto" + } + ], + "description": "Upload subtitles dto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Subtitle uploaded." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SubtitleManagement", + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{itemId}/Subtitles/{index}": { + "delete": { + "tags": [ + "Subtitle" + ], + "summary": "Deletes an external subtitle file.", + "operationId": "DeleteSubtitle", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "index", + "in": "path", + "description": "The index of the subtitle file.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "Subtitle deleted." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}": { + "get": { + "tags": [ + "Subtitle" + ], + "summary": "Gets subtitles in a specified format.", + "operationId": "GetSubtitleWithTicks", + "parameters": [ + { + "name": "routeItemId", + "in": "path", + "description": "The (route) item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "routeMediaSourceId", + "in": "path", + "description": "The (route) media source id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "routeIndex", + "in": "path", + "description": "The (route) subtitle stream index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "routeStartPositionTicks", + "in": "path", + "description": "The (route) start position of the subtitle in ticks.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "routeFormat", + "in": "path", + "description": "The (route) format of the returned subtitle.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "itemId", + "in": "query", + "description": "The item id.", + "deprecated": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media source id.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "index", + "in": "query", + "description": "The subtitle stream index.", + "deprecated": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "startPositionTicks", + "in": "query", + "description": "The start position of the subtitle in ticks.", + "deprecated": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "format", + "in": "query", + "description": "The format of the returned subtitle.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "endPositionTicks", + "in": "query", + "description": "Optional. The end position of the subtitle in ticks.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Optional. Whether to copy the timestamps.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "addVttTimeMap", + "in": "query", + "description": "Optional. Whether to add a VTT time map.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "File returned.", + "content": { + "text/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}": { + "get": { + "tags": [ + "Subtitle" + ], + "summary": "Gets subtitles in a specified format.", + "operationId": "GetSubtitle", + "parameters": [ + { + "name": "routeItemId", + "in": "path", + "description": "The (route) item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "routeMediaSourceId", + "in": "path", + "description": "The (route) media source id.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "routeIndex", + "in": "path", + "description": "The (route) subtitle stream index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "routeFormat", + "in": "path", + "description": "The (route) format of the returned subtitle.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "itemId", + "in": "query", + "description": "The item id.", + "deprecated": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media source id.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "index", + "in": "query", + "description": "The subtitle stream index.", + "deprecated": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "format", + "in": "query", + "description": "The format of the returned subtitle.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "endPositionTicks", + "in": "query", + "description": "Optional. The end position of the subtitle in ticks.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Optional. Whether to copy the timestamps.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "addVttTimeMap", + "in": "query", + "description": "Optional. Whether to add a VTT time map.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "startPositionTicks", + "in": "query", + "description": "The start position of the subtitle in ticks.", + "schema": { + "type": "integer", + "format": "int64", + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "File returned.", + "content": { + "text/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Items/Suggestions": { + "get": { + "tags": [ + "Suggestions" + ], + "summary": "Gets suggestions.", + "operationId": "GetSuggestions", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "mediaType", + "in": "query", + "description": "The media types.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + } + } + }, + { + "name": "type", + "in": "query", + "description": "The type.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The start index.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The limit.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Whether to enable the total record count.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Suggestions returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/{id}": { + "get": { + "tags": [ + "SyncPlay" + ], + "summary": "Gets a SyncPlay group by id.", + "operationId": "SyncPlayGetGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the group.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Group returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupInfoDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/GroupInfoDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/GroupInfoDto" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayJoinGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/Buffering": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Notify SyncPlay group that member is buffering.", + "operationId": "SyncPlayBuffering", + "requestBody": { + "description": "The player status.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BufferRequestDto" + } + ], + "description": "Class BufferRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BufferRequestDto" + } + ], + "description": "Class BufferRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/BufferRequestDto" + } + ], + "description": "Class BufferRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Group state update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/Join": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Join an existing SyncPlay group.", + "operationId": "SyncPlayJoinGroup", + "requestBody": { + "description": "The group to join.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/JoinGroupRequestDto" + } + ], + "description": "Class JoinGroupRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/JoinGroupRequestDto" + } + ], + "description": "Class JoinGroupRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/JoinGroupRequestDto" + } + ], + "description": "Class JoinGroupRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Group join successful." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayJoinGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/Leave": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Leave the joined SyncPlay group.", + "operationId": "SyncPlayLeaveGroup", + "responses": { + "204": { + "description": "Group leave successful." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/List": { + "get": { + "tags": [ + "SyncPlay" + ], + "summary": "Gets all SyncPlay groups.", + "operationId": "SyncPlayGetGroups", + "responses": { + "200": { + "description": "Groups returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupInfoDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupInfoDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupInfoDto" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayJoinGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/MovePlaylistItem": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request to move an item in the playlist in SyncPlay group.", + "operationId": "SyncPlayMovePlaylistItem", + "requestBody": { + "description": "The new position for the item.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MovePlaylistItemRequestDto" + } + ], + "description": "Class MovePlaylistItemRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MovePlaylistItemRequestDto" + } + ], + "description": "Class MovePlaylistItemRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MovePlaylistItemRequestDto" + } + ], + "description": "Class MovePlaylistItemRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Queue update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/New": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Create a new SyncPlay group.", + "operationId": "SyncPlayCreateGroup", + "requestBody": { + "description": "The settings of the new group.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/NewGroupRequestDto" + } + ], + "description": "Class NewGroupRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/NewGroupRequestDto" + } + ], + "description": "Class NewGroupRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/NewGroupRequestDto" + } + ], + "description": "Class NewGroupRequestDto." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupInfoDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/GroupInfoDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/GroupInfoDto" + } + } + } + }, + "204": { + "description": "New group created." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayCreateGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/NextItem": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request next item in SyncPlay group.", + "operationId": "SyncPlayNextItem", + "requestBody": { + "description": "The current item information.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/NextItemRequestDto" + } + ], + "description": "Class NextItemRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/NextItemRequestDto" + } + ], + "description": "Class NextItemRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/NextItemRequestDto" + } + ], + "description": "Class NextItemRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Next item update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/Pause": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request pause in SyncPlay group.", + "operationId": "SyncPlayPause", + "responses": { + "204": { + "description": "Pause update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/Ping": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Update session ping.", + "operationId": "SyncPlayPing", + "requestBody": { + "description": "The new ping.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PingRequestDto" + } + ], + "description": "Class PingRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PingRequestDto" + } + ], + "description": "Class PingRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PingRequestDto" + } + ], + "description": "Class PingRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Ping updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/PreviousItem": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request previous item in SyncPlay group.", + "operationId": "SyncPlayPreviousItem", + "requestBody": { + "description": "The current item information.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PreviousItemRequestDto" + } + ], + "description": "Class PreviousItemRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PreviousItemRequestDto" + } + ], + "description": "Class PreviousItemRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PreviousItemRequestDto" + } + ], + "description": "Class PreviousItemRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Previous item update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/Queue": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request to queue items to the playlist of a SyncPlay group.", + "operationId": "SyncPlayQueue", + "requestBody": { + "description": "The items to add.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/QueueRequestDto" + } + ], + "description": "Class QueueRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/QueueRequestDto" + } + ], + "description": "Class QueueRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/QueueRequestDto" + } + ], + "description": "Class QueueRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Queue update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/Ready": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Notify SyncPlay group that member is ready for playback.", + "operationId": "SyncPlayReady", + "requestBody": { + "description": "The player status.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ReadyRequestDto" + } + ], + "description": "Class ReadyRequest." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ReadyRequestDto" + } + ], + "description": "Class ReadyRequest." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ReadyRequestDto" + } + ], + "description": "Class ReadyRequest." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Group state update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/RemoveFromPlaylist": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request to remove items from the playlist in SyncPlay group.", + "operationId": "SyncPlayRemoveFromPlaylist", + "requestBody": { + "description": "The items to remove.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/RemoveFromPlaylistRequestDto" + } + ], + "description": "Class RemoveFromPlaylistRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/RemoveFromPlaylistRequestDto" + } + ], + "description": "Class RemoveFromPlaylistRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/RemoveFromPlaylistRequestDto" + } + ], + "description": "Class RemoveFromPlaylistRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Queue update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/Seek": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request seek in SyncPlay group.", + "operationId": "SyncPlaySeek", + "requestBody": { + "description": "The new playback position.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeekRequestDto" + } + ], + "description": "Class SeekRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeekRequestDto" + } + ], + "description": "Class SeekRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SeekRequestDto" + } + ], + "description": "Class SeekRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Seek update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/SetIgnoreWait": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request SyncPlay group to ignore member during group-wait.", + "operationId": "SyncPlaySetIgnoreWait", + "requestBody": { + "description": "The settings to set.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/IgnoreWaitRequestDto" + } + ], + "description": "Class IgnoreWaitRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/IgnoreWaitRequestDto" + } + ], + "description": "Class IgnoreWaitRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/IgnoreWaitRequestDto" + } + ], + "description": "Class IgnoreWaitRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Member state updated." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/SetNewQueue": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request to set new playlist in SyncPlay group.", + "operationId": "SyncPlaySetNewQueue", + "requestBody": { + "description": "The new playlist to play in the group.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlayRequestDto" + } + ], + "description": "Class PlayRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlayRequestDto" + } + ], + "description": "Class PlayRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PlayRequestDto" + } + ], + "description": "Class PlayRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Queue update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/SetPlaylistItem": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request to change playlist item in SyncPlay group.", + "operationId": "SyncPlaySetPlaylistItem", + "requestBody": { + "description": "The new item to play.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetPlaylistItemRequestDto" + } + ], + "description": "Class SetPlaylistItemRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetPlaylistItemRequestDto" + } + ], + "description": "Class SetPlaylistItemRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetPlaylistItemRequestDto" + } + ], + "description": "Class SetPlaylistItemRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Queue update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/SetRepeatMode": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request to set repeat mode in SyncPlay group.", + "operationId": "SyncPlaySetRepeatMode", + "requestBody": { + "description": "The new repeat mode.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetRepeatModeRequestDto" + } + ], + "description": "Class SetRepeatModeRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetRepeatModeRequestDto" + } + ], + "description": "Class SetRepeatModeRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetRepeatModeRequestDto" + } + ], + "description": "Class SetRepeatModeRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Play queue update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/SetShuffleMode": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request to set shuffle mode in SyncPlay group.", + "operationId": "SyncPlaySetShuffleMode", + "requestBody": { + "description": "The new shuffle mode.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetShuffleModeRequestDto" + } + ], + "description": "Class SetShuffleModeRequestDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetShuffleModeRequestDto" + } + ], + "description": "Class SetShuffleModeRequestDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/SetShuffleModeRequestDto" + } + ], + "description": "Class SetShuffleModeRequestDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Play queue update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/Stop": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request stop in SyncPlay group.", + "operationId": "SyncPlayStop", + "responses": { + "204": { + "description": "Stop update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/SyncPlay/Unpause": { + "post": { + "tags": [ + "SyncPlay" + ], + "summary": "Request unpause in SyncPlay group.", + "operationId": "SyncPlayUnpause", + "responses": { + "204": { + "description": "Unpause update sent to all group members." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "SyncPlayIsInGroup", + "SyncPlayHasAccess", + "DefaultAuthorization" + ] + } + ] + } + }, + "/System/Endpoint": { + "get": { + "tags": [ + "System" + ], + "summary": "Gets information about the request endpoint.", + "operationId": "GetEndpointInfo", + "responses": { + "200": { + "description": "Information retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EndPointInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/EndPointInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/EndPointInfo" + } + } + } + }, + "403": { + "description": "User does not have permission to get endpoint information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/System/Info": { + "get": { + "tags": [ + "System" + ], + "summary": "Gets information about the server.", + "operationId": "GetSystemInfo", + "responses": { + "200": { + "description": "Information retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/SystemInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/SystemInfo" + } + } + } + }, + "403": { + "description": "User does not have permission to retrieve information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "FirstTimeSetupOrIgnoreParentalControl", + "DefaultAuthorization" + ] + } + ] + } + }, + "/System/Info/Public": { + "get": { + "tags": [ + "System" + ], + "summary": "Gets public information about the server.", + "operationId": "GetPublicSystemInfo", + "responses": { + "200": { + "description": "Information retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicSystemInfo" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/PublicSystemInfo" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/PublicSystemInfo" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/System/Info/Storage": { + "get": { + "tags": [ + "System" + ], + "summary": "Gets information about the server.", + "operationId": "GetSystemStorage", + "responses": { + "200": { + "description": "Information retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemStorageDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/SystemStorageDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/SystemStorageDto" + } + } + } + }, + "403": { + "description": "User does not have permission to retrieve information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/System/Logs": { + "get": { + "tags": [ + "System" + ], + "summary": "Gets a list of available server log files.", + "operationId": "GetServerLogs", + "responses": { + "200": { + "description": "Information retrieved.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogFile" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogFile" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogFile" + } + } + } + } + }, + "403": { + "description": "User does not have permission to get server logs.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/System/Logs/Log": { + "get": { + "tags": [ + "System" + ], + "summary": "Gets a log file.", + "operationId": "GetLogFile", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The name of the log file to get.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Log file retrieved.", + "content": { + "text/plain": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "403": { + "description": "User does not have permission to get log files.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Could not find a log file with the name.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/System/Ping": { + "get": { + "tags": [ + "System" + ], + "summary": "Pings the system.", + "operationId": "GetPingSystem", + "responses": { + "200": { + "description": "Information retrieved.", + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "string" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "string" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "post": { + "tags": [ + "System" + ], + "summary": "Pings the system.", + "operationId": "PostPingSystem", + "responses": { + "200": { + "description": "Information retrieved.", + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "string" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "string" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/System/Restart": { + "post": { + "tags": [ + "System" + ], + "summary": "Restarts the application.", + "operationId": "RestartApplication", + "responses": { + "204": { + "description": "Server restarted." + }, + "403": { + "description": "User does not have permission to restart server.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "LocalAccessOrRequiresElevation" + ] + } + ] + } + }, + "/System/Shutdown": { + "post": { + "tags": [ + "System" + ], + "summary": "Shuts down the application.", + "operationId": "ShutdownApplication", + "responses": { + "204": { + "description": "Server shut down." + }, + "403": { + "description": "User does not have permission to shutdown server.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/GetUtcTime": { + "get": { + "tags": [ + "TimeSync" + ], + "summary": "Gets the current UTC time.", + "operationId": "GetUtcTime", + "responses": { + "200": { + "description": "Time returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UtcTimeResponse" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UtcTimeResponse" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UtcTimeResponse" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Tmdb/ClientConfiguration": { + "get": { + "tags": [ + "Tmdb" + ], + "summary": "Gets the TMDb image configuration options.", + "operationId": "TmdbClientConfiguration", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConfigImageTypes" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Trailers": { + "get": { + "tags": [ + "Trailers" + ], + "summary": "Finds movies and trailers similar to a given trailer.", + "operationId": "GetTrailers", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id supplied as query parameter; this is required when not using an API key.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "maxOfficialRating", + "in": "query", + "description": "Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).", + "schema": { + "type": "string" + } + }, + { + "name": "hasThemeSong", + "in": "query", + "description": "Optional filter by items with theme songs.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasThemeVideo", + "in": "query", + "description": "Optional filter by items with theme videos.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasSubtitles", + "in": "query", + "description": "Optional filter by items with subtitles.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasSpecialFeature", + "in": "query", + "description": "Optional filter by items with special features.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasTrailer", + "in": "query", + "description": "Optional filter by items with trailers.", + "schema": { + "type": "boolean" + } + }, + { + "name": "adjacentTo", + "in": "query", + "description": "Optional. Return items that are siblings of a supplied item.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "parentIndexNumber", + "in": "query", + "description": "Optional filter by parent index number.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "hasParentalRating", + "in": "query", + "description": "Optional filter by items that have or do not have a parental rating.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isHd", + "in": "query", + "description": "Optional filter by items that are HD or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "is4K", + "in": "query", + "description": "Optional filter by items that are 4K or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "locationTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocationType" + } + } + }, + { + "name": "excludeLocationTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocationType" + } + } + }, + { + "name": "isMissing", + "in": "query", + "description": "Optional filter by items that are missing episodes or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isUnaired", + "in": "query", + "description": "Optional filter by items that are unaired episodes or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "minCommunityRating", + "in": "query", + "description": "Optional filter by minimum community rating.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "minCriticRating", + "in": "query", + "description": "Optional filter by minimum critic rating.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "minPremiereDate", + "in": "query", + "description": "Optional. The minimum premiere date. Format = ISO.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "minDateLastSaved", + "in": "query", + "description": "Optional. The minimum last saved date. Format = ISO.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "minDateLastSavedForUser", + "in": "query", + "description": "Optional. The minimum last saved date for the current user. Format = ISO.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "maxPremiereDate", + "in": "query", + "description": "Optional. The maximum premiere date. Format = ISO.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "hasOverview", + "in": "query", + "description": "Optional filter by items that have an overview or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasImdbId", + "in": "query", + "description": "Optional filter by items that have an IMDb id or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasTmdbId", + "in": "query", + "description": "Optional filter by items that have a TMDb id or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasTvdbId", + "in": "query", + "description": "Optional filter by items that have a TVDb id or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMovie", + "in": "query", + "description": "Optional filter for live tv movies.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSeries", + "in": "query", + "description": "Optional filter for live tv series.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isNews", + "in": "query", + "description": "Optional filter for live tv news.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isKids", + "in": "query", + "description": "Optional filter for live tv kids.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isSports", + "in": "query", + "description": "Optional filter for live tv sports.", + "schema": { + "type": "boolean" + } + }, + { + "name": "excludeItemIds", + "in": "query", + "description": "Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "recursive", + "in": "query", + "description": "When searching within folders, this determines whether or not the search will be recursive. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "searchTerm", + "in": "query", + "description": "Optional. Filter based on a search term.", + "schema": { + "type": "string" + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort Order - Ascending, Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "excludeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "filters", + "in": "query", + "description": "Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFilter" + } + } + }, + { + "name": "isFavorite", + "in": "query", + "description": "Optional filter by items that are marked as favorite, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "mediaTypes", + "in": "query", + "description": "Optional filter by MediaType. Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + } + } + }, + { + "name": "imageTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "isPlayed", + "in": "query", + "description": "Optional filter by items that are played, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "genres", + "in": "query", + "description": "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "officialRatings", + "in": "query", + "description": "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "tags", + "in": "query", + "description": "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "years", + "in": "query", + "description": "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional, include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional, the max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "person", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified person.", + "schema": { + "type": "string" + } + }, + { + "name": "personIds", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified person id.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "personTypes", + "in": "query", + "description": "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "studios", + "in": "query", + "description": "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "artists", + "in": "query", + "description": "Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "excludeArtistIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "artistIds", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified artist id.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "albumArtistIds", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified album artist id.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "contributingArtistIds", + "in": "query", + "description": "Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "albums", + "in": "query", + "description": "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "albumIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "ids", + "in": "query", + "description": "Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "videoTypes", + "in": "query", + "description": "Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VideoType" + } + } + }, + { + "name": "minOfficialRating", + "in": "query", + "description": "Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).", + "schema": { + "type": "string" + } + }, + { + "name": "isLocked", + "in": "query", + "description": "Optional filter by items that are locked.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isPlaceHolder", + "in": "query", + "description": "Optional filter by items that are placeholders.", + "schema": { + "type": "boolean" + } + }, + { + "name": "hasOfficialRating", + "in": "query", + "description": "Optional filter by items that have official ratings.", + "schema": { + "type": "boolean" + } + }, + { + "name": "collapseBoxSetItems", + "in": "query", + "description": "Whether or not to hide items behind their boxsets.", + "schema": { + "type": "boolean" + } + }, + { + "name": "minWidth", + "in": "query", + "description": "Optional. Filter by the minimum width of the item.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minHeight", + "in": "query", + "description": "Optional. Filter by the minimum height of the item.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. Filter by the maximum width of the item.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. Filter by the maximum height of the item.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "is3D", + "in": "query", + "description": "Optional filter by items that are 3D, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "seriesStatus", + "in": "query", + "description": "Optional filter by Series Status. Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesStatus" + } + } + }, + { + "name": "nameStartsWithOrGreater", + "in": "query", + "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameStartsWith", + "in": "query", + "description": "Optional filter by items whose name is sorted equally than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "nameLessThan", + "in": "query", + "description": "Optional filter by items whose name is equally or lesser than a given input string.", + "schema": { + "type": "string" + } + }, + { + "name": "studioIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "genreIds", + "in": "query", + "description": "Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Optional. Enable the total record count.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional, include image information in output.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{itemId}/Trickplay/{width}/{index}.jpg": { + "get": { + "tags": [ + "Trickplay" + ], + "summary": "Gets a trickplay tile image.", + "operationId": "GetTrickplayTileImage", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "width", + "in": "path", + "description": "The width of a single tile.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "index", + "in": "path", + "description": "The index of the desired tile.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if using an alternate version.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Tile image not found at specified index.", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{itemId}/Trickplay/{width}/tiles.m3u8": { + "get": { + "tags": [ + "Trickplay" + ], + "summary": "Gets an image tiles playlist for trickplay.", + "operationId": "GetTrickplayHlsPlaylist", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "width", + "in": "path", + "description": "The width of a single tile.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if using an alternate version.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Tiles playlist returned.", + "content": { + "application/x-mpegURL": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Shows/{seriesId}/Episodes": { + "get": { + "tags": [ + "TvShows" + ], + "summary": "Gets episodes for a tv season.", + "operationId": "GetEpisodes", + "parameters": [ + { + "name": "seriesId", + "in": "path", + "description": "The series id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "season", + "in": "query", + "description": "Optional filter by season number.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "seasonId", + "in": "query", + "description": "Optional. Filter by season id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "isMissing", + "in": "query", + "description": "Optional. Filter by items that are missing episodes or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "adjacentTo", + "in": "query", + "description": "Optional. Return items that are siblings of a supplied item.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startItemId", + "in": "query", + "description": "Optional. Skip through the list until a given item is found.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional, include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional, the max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", + "schema": { + "enum": [ + "Default", + "AiredEpisodeOrder", + "Album", + "AlbumArtist", + "Artist", + "DateCreated", + "OfficialRating", + "DatePlayed", + "PremiereDate", + "StartDate", + "SortName", + "Name", + "Random", + "Runtime", + "CommunityRating", + "ProductionYear", + "PlayCount", + "CriticRating", + "IsFolder", + "IsUnplayed", + "IsPlayed", + "SeriesSortName", + "VideoBitRate", + "AirTime", + "Studio", + "IsFavoriteOrLiked", + "DateLastContentAdded", + "SeriesDatePlayed", + "ParentIndexNumber", + "IndexNumber" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ItemSortBy" + } + ] + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Shows/{seriesId}/Seasons": { + "get": { + "tags": [ + "TvShows" + ], + "summary": "Gets seasons for a tv series.", + "operationId": "GetSeasons", + "parameters": [ + { + "name": "seriesId", + "in": "path", + "description": "The series id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "isSpecialSeason", + "in": "query", + "description": "Optional. Filter by special season.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMissing", + "in": "query", + "description": "Optional. Filter by items that are missing episodes or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "adjacentTo", + "in": "query", + "description": "Optional. Return items that are siblings of a supplied item.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Shows/NextUp": { + "get": { + "tags": [ + "TvShows" + ], + "summary": "Gets a list of next up episodes.", + "operationId": "GetNextUp", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id of the user to get the next up episodes for.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "seriesId", + "in": "query", + "description": "Optional. Filter by series id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "nextUpDateCutoff", + "in": "query", + "description": "Optional. Starting date of shows to show in Next Up section.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "enableTotalRecordCount", + "in": "query", + "description": "Whether to enable the total records count. Defaults to true.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "disableFirstEpisode", + "in": "query", + "description": "Whether to disable sending the first episode in a series as next up.", + "deprecated": true, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "enableResumable", + "in": "query", + "description": "Whether to include resumable episodes in next up results.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableRewatching", + "in": "query", + "description": "Whether to include watched episodes in next up results.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Shows/Upcoming": { + "get": { + "tags": [ + "TvShows" + ], + "summary": "Gets a list of upcoming episodes.", + "operationId": "GetUpcomingEpisodes", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id of the user to get the upcoming episodes for.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "startIndex", + "in": "query", + "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "parentId", + "in": "query", + "description": "Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Audio/{itemId}/universal": { + "get": { + "tags": [ + "UniversalAudio" + ], + "summary": "Gets an audio stream.", + "operationId": "GetUniversalAudioStream", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "query", + "description": "Optional. The audio container.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. The audio codec to transcode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "transcodingAudioChannels", + "in": "query", + "description": "Optional. The number of how many audio channels to transcode to.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxStreamingBitrate", + "in": "query", + "description": "Optional. The maximum streaming bitrate.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "transcodingContainer", + "in": "query", + "description": "Optional. The container to transcode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodingProtocol", + "in": "query", + "description": "Optional. The transcoding protocol.", + "schema": { + "enum": [ + "http", + "hls" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaStreamProtocol" + } + ] + } + }, + { + "name": "maxAudioSampleRate", + "in": "query", + "description": "Optional. The maximum audio sample rate.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableRemoteMedia", + "in": "query", + "description": "Optional. Whether to enable remote media.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "enableRedirection", + "in": "query", + "description": "Whether to enable redirection. Defaults to true.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Audio stream returned.", + "content": { + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "302": { + "description": "Redirected to remote audio stream." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "head": { + "tags": [ + "UniversalAudio" + ], + "summary": "Gets an audio stream.", + "operationId": "HeadUniversalAudioStream", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "query", + "description": "Optional. The audio container.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. The audio codec to transcode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "transcodingAudioChannels", + "in": "query", + "description": "Optional. The number of how many audio channels to transcode to.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxStreamingBitrate", + "in": "query", + "description": "Optional. The maximum streaming bitrate.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "transcodingContainer", + "in": "query", + "description": "Optional. The container to transcode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodingProtocol", + "in": "query", + "description": "Optional. The transcoding protocol.", + "schema": { + "enum": [ + "http", + "hls" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaStreamProtocol" + } + ] + } + }, + { + "name": "maxAudioSampleRate", + "in": "query", + "description": "Optional. The maximum audio sample rate.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableRemoteMedia", + "in": "query", + "description": "Optional. Whether to enable remote media.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "enableRedirection", + "in": "query", + "description": "Whether to enable redirection. Defaults to true.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Audio stream returned.", + "content": { + "audio/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "302": { + "description": "Redirected to remote audio stream." + }, + "404": { + "description": "Item not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Users": { + "get": { + "tags": [ + "User" + ], + "summary": "Gets a list of users.", + "operationId": "GetUsers", + "parameters": [ + { + "name": "isHidden", + "in": "query", + "description": "Optional filter by IsHidden=true or false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "isDisabled", + "in": "query", + "description": "Optional filter by IsDisabled=true or false.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Users returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDto" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "User" + ], + "summary": "Updates a user.", + "operationId": "UpdateUser", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The updated user model.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UserDto" + } + ], + "description": "Class UserDto." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UserDto" + } + ], + "description": "Class UserDto." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UserDto" + } + ], + "description": "Class UserDto." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "User updated." + }, + "400": { + "description": "User information was not supplied.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "403": { + "description": "User update forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Users/{userId}": { + "get": { + "tags": [ + "User" + ], + "summary": "Gets a user by Id.", + "operationId": "GetUserById", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "The user id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "User returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + } + } + }, + "404": { + "description": "User not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "IgnoreParentalControl", + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "User" + ], + "summary": "Deletes a user.", + "operationId": "DeleteUser", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "The user id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "User deleted." + }, + "404": { + "description": "User not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Users/{userId}/Policy": { + "post": { + "tags": [ + "User" + ], + "summary": "Updates a user policy.", + "operationId": "UpdateUserPolicy", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "The user id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The new user policy.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UserPolicy" + } + ] + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UserPolicy" + } + ] + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UserPolicy" + } + ] + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "User policy updated." + }, + "400": { + "description": "User policy was not supplied.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "403": { + "description": "User policy update forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Users/AuthenticateByName": { + "post": { + "tags": [ + "User" + ], + "summary": "Authenticates a user by name.", + "operationId": "AuthenticateUserByName", + "requestBody": { + "description": "The M:Jellyfin.Api.Controllers.UserController.AuthenticateUserByName(Jellyfin.Api.Models.UserDtos.AuthenticateUserByName) request.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticateUserByName" + } + ], + "description": "The authenticate user by name request body." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticateUserByName" + } + ], + "description": "The authenticate user by name request body." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticateUserByName" + } + ], + "description": "The authenticate user by name request body." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User authenticated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/AuthenticationResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/AuthenticationResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Users/AuthenticateWithQuickConnect": { + "post": { + "tags": [ + "User" + ], + "summary": "Authenticates a user with quick connect.", + "operationId": "AuthenticateWithQuickConnect", + "requestBody": { + "description": "The Jellyfin.Api.Models.UserDtos.QuickConnectDto request.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/QuickConnectDto" + } + ], + "description": "The quick connect request body." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/QuickConnectDto" + } + ], + "description": "The quick connect request body." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/QuickConnectDto" + } + ], + "description": "The quick connect request body." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User authenticated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/AuthenticationResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/AuthenticationResult" + } + } + } + }, + "400": { + "description": "Missing token." + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Users/Configuration": { + "post": { + "tags": [ + "User" + ], + "summary": "Updates a user configuration.", + "operationId": "UpdateUserConfiguration", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The new user configuration.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UserConfiguration" + } + ], + "description": "Class UserConfiguration." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UserConfiguration" + } + ], + "description": "Class UserConfiguration." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UserConfiguration" + } + ], + "description": "Class UserConfiguration." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "User configuration updated." + }, + "403": { + "description": "User configuration update forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Users/ForgotPassword": { + "post": { + "tags": [ + "User" + ], + "summary": "Initiates the forgot password process for a local user.", + "operationId": "ForgotPassword", + "requestBody": { + "description": "The forgot password request containing the entered username.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ForgotPasswordDto" + } + ], + "description": "Forgot Password request body DTO." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ForgotPasswordDto" + } + ], + "description": "Forgot Password request body DTO." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ForgotPasswordDto" + } + ], + "description": "Forgot Password request body DTO." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password reset process started.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForgotPasswordResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ForgotPasswordResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ForgotPasswordResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Users/ForgotPassword/Pin": { + "post": { + "tags": [ + "User" + ], + "summary": "Redeems a forgot password pin.", + "operationId": "ForgotPasswordPin", + "requestBody": { + "description": "The forgot password pin request containing the entered pin.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ForgotPasswordPinDto" + } + ], + "description": "Forgot Password Pin enter request body DTO." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ForgotPasswordPinDto" + } + ], + "description": "Forgot Password Pin enter request body DTO." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ForgotPasswordPinDto" + } + ], + "description": "Forgot Password Pin enter request body DTO." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Pin reset process started.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PinRedeemResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/PinRedeemResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/PinRedeemResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Users/Me": { + "get": { + "tags": [ + "User" + ], + "summary": "Gets the user based on auth token.", + "operationId": "GetCurrentUser", + "responses": { + "200": { + "description": "User returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + } + } + }, + "400": { + "description": "Token is not owned by a user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Users/New": { + "post": { + "tags": [ + "User" + ], + "summary": "Creates a user.", + "operationId": "CreateUserByName", + "requestBody": { + "description": "The create user by name request body.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/CreateUserByName" + } + ], + "description": "The create user by name request body." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/CreateUserByName" + } + ], + "description": "The create user by name request body." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/CreateUserByName" + } + ], + "description": "The create user by name request body." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Users/Password": { + "post": { + "tags": [ + "User" + ], + "summary": "Updates a user's password.", + "operationId": "UpdateUserPassword", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "The user id.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The M:Jellyfin.Api.Controllers.UserController.UpdateUserPassword(System.Nullable{System.Guid},Jellyfin.Api.Models.UserDtos.UpdateUserPassword) request.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateUserPassword" + } + ], + "description": "The update user password request body." + } + }, + "text/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateUserPassword" + } + ], + "description": "The update user password request body." + } + }, + "application/*+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UpdateUserPassword" + } + ], + "description": "The update user password request body." + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Password successfully reset." + }, + "403": { + "description": "User is not allowed to update the password.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "User not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Users/Public": { + "get": { + "tags": [ + "User" + ], + "summary": "Gets a list of publicly visible users for display on a login screen.", + "operationId": "GetPublicUsers", + "responses": { + "200": { + "description": "Public users returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDto" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Items/{itemId}/Intros": { + "get": { + "tags": [ + "UserLibrary" + ], + "summary": "Gets intros to play before the main media item plays.", + "operationId": "GetIntros", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Intros returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/LocalTrailers": { + "get": { + "tags": [ + "UserLibrary" + ], + "summary": "Gets local trailers for an item.", + "operationId": "GetLocalTrailers", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "An Microsoft.AspNetCore.Mvc.OkResult containing the item's local trailers.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/{itemId}/SpecialFeatures": { + "get": { + "tags": [ + "UserLibrary" + ], + "summary": "Gets special features for an item.", + "operationId": "GetSpecialFeatures", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Special features returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/Latest": { + "get": { + "tags": [ + "UserLibrary" + ], + "summary": "Gets latest media.", + "operationId": "GetLatestMedia", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "isPlayed", + "in": "query", + "description": "Filter by items that are played, or not.", + "schema": { + "type": "boolean" + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. include image information in output.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. the max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "description": "Return item limit.", + "schema": { + "type": "integer", + "format": "int32", + "default": 20 + } + }, + { + "name": "groupItems", + "in": "query", + "description": "Whether or not to group items into a parent container.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Latest media returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Items/Root": { + "get": { + "tags": [ + "UserLibrary" + ], + "summary": "Gets the root folder from a user's library.", + "operationId": "GetRootFolder", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Root folder returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/UserFavoriteItems/{itemId}": { + "post": { + "tags": [ + "UserLibrary" + ], + "summary": "Marks an item as a favorite.", + "operationId": "MarkFavoriteItem", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Item marked as favorite.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "delete": { + "tags": [ + "UserLibrary" + ], + "summary": "Unmarks item as a favorite.", + "operationId": "UnmarkFavoriteItem", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Item unmarked as favorite.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/UserItems/{itemId}/Rating": { + "delete": { + "tags": [ + "UserLibrary" + ], + "summary": "Deletes a user's saved personal rating for an item.", + "operationId": "DeleteUserItemRating", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Personal rating removed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + }, + "post": { + "tags": [ + "UserLibrary" + ], + "summary": "Updates a user's rating for an item.", + "operationId": "UpdateUserItemRating", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "itemId", + "in": "path", + "description": "Item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "likes", + "in": "query", + "description": "Whether this M:Jellyfin.Api.Controllers.UserLibraryController.UpdateUserItemRating(System.Nullable{System.Guid},System.Guid,System.Nullable{System.Boolean}) is likes.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Item rating updated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/UserItemDataDto" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/UserViews": { + "get": { + "tags": [ + "UserViews" + ], + "summary": "Get user views.", + "operationId": "GetUserViews", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "includeExternalContent", + "in": "query", + "description": "Whether or not to include external views such as channels or live tv.", + "schema": { + "type": "boolean" + } + }, + { + "name": "presetViews", + "in": "query", + "description": "Preset views.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CollectionType" + } + } + }, + { + "name": "includeHidden", + "in": "query", + "description": "Whether or not to include hidden content.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "User views returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/UserViews/GroupingOptions": { + "get": { + "tags": [ + "UserViews" + ], + "summary": "Get user view grouping options.", + "operationId": "GetGroupingOptions", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "User id.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "User view grouping options returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SpecialViewOptionDto" + } + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SpecialViewOptionDto" + } + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SpecialViewOptionDto" + } + } + } + } + }, + "404": { + "description": "User not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{videoId}/{mediaSourceId}/Attachments/{index}": { + "get": { + "tags": [ + "VideoAttachments" + ], + "summary": "Get video attachment.", + "operationId": "GetAttachment", + "parameters": [ + { + "name": "videoId", + "in": "path", + "description": "Video ID.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "mediaSourceId", + "in": "path", + "description": "Media Source ID.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "index", + "in": "path", + "description": "Attachment Index.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Attachment retrieved.", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Video or attachment not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Videos/{itemId}/AdditionalParts": { + "get": { + "tags": [ + "Videos" + ], + "summary": "Gets additional parts for a video.", + "operationId": "GetAdditionalPart", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Additional parts returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Videos/{itemId}/AlternateSources": { + "delete": { + "tags": [ + "Videos" + ], + "summary": "Removes alternate video sources.", + "operationId": "DeleteAlternateSources", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Alternate sources deleted." + }, + "404": { + "description": "Video not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Videos/{itemId}/stream": { + "get": { + "tags": [ + "Videos" + ], + "summary": "Gets a video stream.", + "operationId": "GetVideoStream", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "query", + "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. The maximum horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. The maximum vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Video stream returned.", + "content": { + "video/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Videos" + ], + "summary": "Gets a video stream.", + "operationId": "HeadVideoStream", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "query", + "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "deprecated": true, + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. The maximum horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. The maximum vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Video stream returned.", + "content": { + "video/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Videos/{itemId}/stream.{container}": { + "get": { + "tags": [ + "Videos" + ], + "summary": "Gets a video stream.", + "operationId": "GetVideoStreamByContainer", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "path", + "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. The maximum horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. The maximum vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Video stream returned.", + "content": { + "video/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + }, + "head": { + "tags": [ + "Videos" + ], + "summary": "Gets a video stream.", + "operationId": "HeadVideoStreamByContainer", + "parameters": [ + { + "name": "itemId", + "in": "path", + "description": "The item id.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "container", + "in": "path", + "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", + "required": true, + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "static", + "in": "query", + "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "params", + "in": "query", + "description": "The streaming parameters.", + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "The tag.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceProfileId", + "in": "query", + "description": "Optional. The dlna device profile id to utilize.", + "schema": { + "type": "string" + } + }, + { + "name": "playSessionId", + "in": "query", + "description": "The play session id.", + "schema": { + "type": "string" + } + }, + { + "name": "segmentContainer", + "in": "query", + "description": "The segment container.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "segmentLength", + "in": "query", + "description": "The segment length.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "minSegments", + "in": "query", + "description": "The minimum number of segments.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "mediaSourceId", + "in": "query", + "description": "The media version id, if playing an alternate version.", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "in": "query", + "description": "The device id of the client requesting. Used to stop encoding processes when needed.", + "schema": { + "type": "string" + } + }, + { + "name": "audioCodec", + "in": "query", + "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "enableAutoStreamCopy", + "in": "query", + "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowVideoStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the video stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "allowAudioStreamCopy", + "in": "query", + "description": "Whether or not to allow copying of the audio stream url.", + "schema": { + "type": "boolean" + } + }, + { + "name": "breakOnNonKeyFrames", + "in": "query", + "description": "Optional. Whether to break on non key frames.", + "schema": { + "type": "boolean" + } + }, + { + "name": "audioSampleRate", + "in": "query", + "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioBitDepth", + "in": "query", + "description": "Optional. The maximum audio bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioBitRate", + "in": "query", + "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "audioChannels", + "in": "query", + "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxAudioChannels", + "in": "query", + "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "profile", + "in": "query", + "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", + "schema": { + "pattern": "-?[0-9]+(?:\\.[0-9]+)?", + "type": "string" + } + }, + { + "name": "framerate", + "in": "query", + "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "maxFramerate", + "in": "query", + "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "copyTimestamps", + "in": "query", + "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", + "schema": { + "type": "boolean" + } + }, + { + "name": "startTimeTicks", + "in": "query", + "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "width", + "in": "query", + "description": "Optional. The fixed horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "description": "Optional. The fixed vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxWidth", + "in": "query", + "description": "Optional. The maximum horizontal resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxHeight", + "in": "query", + "description": "Optional. The maximum vertical resolution of the encoded video.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoBitRate", + "in": "query", + "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleStreamIndex", + "in": "query", + "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subtitleMethod", + "in": "query", + "description": "Optional. Specify the subtitle delivery method.", + "schema": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ] + } + }, + { + "name": "maxRefFrames", + "in": "query", + "description": "Optional.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "maxVideoBitDepth", + "in": "query", + "description": "Optional. The maximum video bit depth.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "requireAvc", + "in": "query", + "description": "Optional. Whether to require avc.", + "schema": { + "type": "boolean" + } + }, + { + "name": "deInterlace", + "in": "query", + "description": "Optional. Whether to deinterlace the video.", + "schema": { + "type": "boolean" + } + }, + { + "name": "requireNonAnamorphic", + "in": "query", + "description": "Optional. Whether to require a non anamorphic stream.", + "schema": { + "type": "boolean" + } + }, + { + "name": "transcodingMaxAudioChannels", + "in": "query", + "description": "Optional. The maximum number of audio channels to transcode.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "cpuCoreLimit", + "in": "query", + "description": "Optional. The limit of how many cpu cores to use.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "liveStreamId", + "in": "query", + "description": "The live stream id.", + "schema": { + "type": "string" + } + }, + { + "name": "enableMpegtsM2TsMode", + "in": "query", + "description": "Optional. Whether to enable the MpegtsM2Ts mode.", + "schema": { + "type": "boolean" + } + }, + { + "name": "videoCodec", + "in": "query", + "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "subtitleCodec", + "in": "query", + "description": "Optional. Specify a subtitle codec to encode to.", + "schema": { + "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", + "type": "string" + } + }, + { + "name": "transcodeReasons", + "in": "query", + "description": "Optional. The transcoding reason.", + "schema": { + "type": "string" + } + }, + { + "name": "audioStreamIndex", + "in": "query", + "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "videoStreamIndex", + "in": "query", + "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "context", + "in": "query", + "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", + "schema": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ] + } + }, + { + "name": "streamOptions", + "in": "query", + "description": "Optional. The streaming options.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + { + "name": "enableAudioVbrEncoding", + "in": "query", + "description": "Optional. Whether to enable Audio Encoding.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Video stream returned.", + "content": { + "video/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + } + } + } + }, + "/Videos/MergeVersions": { + "post": { + "tags": [ + "Videos" + ], + "summary": "Merges videos into a single record.", + "operationId": "MergeVersions", + "parameters": [ + { + "name": "ids", + "in": "query", + "description": "Item id list. This allows multiple, comma delimited.", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + ], + "responses": { + "204": { + "description": "Videos merged." + }, + "400": { + "description": "Supply at least 2 video ids.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "RequiresElevation" + ] + } + ] + } + }, + "/Years": { + "get": { + "tags": [ + "Years" + ], + "summary": "Get years.", + "operationId": "GetYears", + "parameters": [ + { + "name": "startIndex", + "in": "query", + "description": "Skips over a given number of items within the results. Use for paging.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "description": "Optional. The maximum number of records to return.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort Order - Ascending,Descending.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + } + } + }, + { + "name": "parentId", + "in": "query", + "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "fields", + "in": "query", + "description": "Optional. Specify additional fields of information to return in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + } + } + }, + { + "name": "excludeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "includeItemTypes", + "in": "query", + "description": "Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemKind" + } + } + }, + { + "name": "mediaTypes", + "in": "query", + "description": "Optional. Filter by MediaType. Allows multiple, comma delimited.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + } + } + }, + { + "name": "sortBy", + "in": "query", + "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + } + } + }, + { + "name": "enableUserData", + "in": "query", + "description": "Optional. Include user data.", + "schema": { + "type": "boolean" + } + }, + { + "name": "imageTypeLimit", + "in": "query", + "description": "Optional. The max number of images to return, per image type.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "enableImageTypes", + "in": "query", + "description": "Optional. The image types to include in the output.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + } + } + }, + { + "name": "userId", + "in": "query", + "description": "User Id.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "recursive", + "in": "query", + "description": "Search recursively.", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "enableImages", + "in": "query", + "description": "Optional. Include image information in output.", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Year query returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDtoQueryResult" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + }, + "/Years/{year}": { + "get": { + "tags": [ + "Years" + ], + "summary": "Gets a year.", + "operationId": "GetYear", + "parameters": [ + { + "name": "year", + "in": "path", + "description": "The year.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "userId", + "in": "query", + "description": "Optional. Filter by user id, and attach user data.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Year returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/BaseItemDto" + } + } + } + }, + "404": { + "description": "Year not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"CamelCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json; profile=\"PascalCase\"": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "The server is currently starting or is temporarily not available.", + "headers": { + "Retry-After": { + "description": "A hint for when to retry the operation in full seconds.", + "allowEmptyValue": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + "Message": { + "description": "A short plain-text reason why the server is not available.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "text" + } + } + }, + "content": { + "text/html": { } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "CustomAuthentication": [ + "DefaultAuthorization" + ] + } + ] + } + } + }, + "components": { + "schemas": { + "AccessSchedule": { + "type": "object", + "properties": { + "Id": { + "type": "integer", + "description": "Gets the id of this instance.", + "format": "int32", + "readOnly": true + }, + "UserId": { + "type": "string", + "description": "Gets the id of the associated user.", + "format": "uuid" + }, + "DayOfWeek": { + "enum": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Everyday", + "Weekday", + "Weekend" + ], + "allOf": [ + { + "$ref": "#/components/schemas/DynamicDayOfWeek" + } + ], + "description": "Gets or sets the day of week." + }, + "StartHour": { + "type": "number", + "description": "Gets or sets the start hour.", + "format": "double" + }, + "EndHour": { + "type": "number", + "description": "Gets or sets the end hour.", + "format": "double" + } + }, + "additionalProperties": false, + "description": "An entity representing a user's access schedule." + }, + "ActivityLogEntry": { + "type": "object", + "properties": { + "Id": { + "type": "integer", + "description": "Gets or sets the identifier.", + "format": "int64" + }, + "Name": { + "type": "string", + "description": "Gets or sets the name." + }, + "Overview": { + "type": "string", + "description": "Gets or sets the overview.", + "nullable": true + }, + "ShortOverview": { + "type": "string", + "description": "Gets or sets the short overview.", + "nullable": true + }, + "Type": { + "type": "string", + "description": "Gets or sets the type." + }, + "ItemId": { + "type": "string", + "description": "Gets or sets the item identifier.", + "nullable": true + }, + "Date": { + "type": "string", + "description": "Gets or sets the date.", + "format": "date-time" + }, + "UserId": { + "type": "string", + "description": "Gets or sets the user identifier.", + "format": "uuid" + }, + "UserPrimaryImageTag": { + "type": "string", + "description": "Gets or sets the user primary image tag.", + "nullable": true, + "deprecated": true + }, + "Severity": { + "enum": [ + "Trace", + "Debug", + "Information", + "Warning", + "Error", + "Critical", + "None" + ], + "allOf": [ + { + "$ref": "#/components/schemas/LogLevel" + } + ], + "description": "Gets or sets the log severity." + } + }, + "additionalProperties": false, + "description": "An activity log entry." + }, + "ActivityLogEntryMessage": { + "type": "object", + "properties": { + "Data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ActivityLogEntry" + }, + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "ActivityLogEntry", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Activity log created message." + }, + "ActivityLogEntryQueryResult": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ActivityLogEntry" + }, + "description": "Gets or sets the items." + }, + "TotalRecordCount": { + "type": "integer", + "description": "Gets or sets the total number of records available.", + "format": "int32" + }, + "StartIndex": { + "type": "integer", + "description": "Gets or sets the index of the first record in Items.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Query result container." + }, + "ActivityLogEntryStartMessage": { + "type": "object", + "properties": { + "Data": { + "type": "string", + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "ActivityLogEntryStart", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Activity log entry start message.\r\nData is the timing data encoded as \"$initialDelay,$interval\" in ms." + }, + "ActivityLogEntryStopMessage": { + "type": "object", + "properties": { + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "ActivityLogEntryStop", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Activity log entry stop message." + }, + "AddVirtualFolderDto": { + "type": "object", + "properties": { + "LibraryOptions": { + "allOf": [ + { + "$ref": "#/components/schemas/LibraryOptions" + } + ], + "description": "Gets or sets library options.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Add virtual folder dto." + }, + "AlbumInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "description": "Gets or sets the original title.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "MetadataLanguage": { + "type": "string", + "description": "Gets or sets the metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "Year": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "IsAutomated": { + "type": "boolean" + }, + "AlbumArtists": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the album artist." + }, + "ArtistProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the artist provider ids." + }, + "SongInfos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SongInfo" + } + } + }, + "additionalProperties": false + }, + "AlbumInfoRemoteSearchQuery": { + "type": "object", + "properties": { + "SearchInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/AlbumInfo" + } + ], + "nullable": true + }, + "ItemId": { + "type": "string", + "format": "uuid" + }, + "SearchProviderName": { + "type": "string", + "description": "Gets or sets the provider name to search within if set.", + "nullable": true + }, + "IncludeDisabledProviders": { + "type": "boolean", + "description": "Gets or sets a value indicating whether disabled providers should be included." + } + }, + "additionalProperties": false + }, + "AllThemeMediaResult": { + "type": "object", + "properties": { + "ThemeVideosResult": { + "allOf": [ + { + "$ref": "#/components/schemas/ThemeMediaResult" + } + ], + "description": "Class ThemeMediaResult.", + "nullable": true + }, + "ThemeSongsResult": { + "allOf": [ + { + "$ref": "#/components/schemas/ThemeMediaResult" + } + ], + "description": "Class ThemeMediaResult.", + "nullable": true + }, + "SoundtrackSongsResult": { + "allOf": [ + { + "$ref": "#/components/schemas/ThemeMediaResult" + } + ], + "description": "Class ThemeMediaResult.", + "nullable": true + } + }, + "additionalProperties": false + }, + "ArtistInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "description": "Gets or sets the original title.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "MetadataLanguage": { + "type": "string", + "description": "Gets or sets the metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "Year": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "IsAutomated": { + "type": "boolean" + }, + "SongInfos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SongInfo" + } + } + }, + "additionalProperties": false + }, + "ArtistInfoRemoteSearchQuery": { + "type": "object", + "properties": { + "SearchInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/ArtistInfo" + } + ], + "nullable": true + }, + "ItemId": { + "type": "string", + "format": "uuid" + }, + "SearchProviderName": { + "type": "string", + "description": "Gets or sets the provider name to search within if set.", + "nullable": true + }, + "IncludeDisabledProviders": { + "type": "boolean", + "description": "Gets or sets a value indicating whether disabled providers should be included." + } + }, + "additionalProperties": false + }, + "AudioSpatialFormat": { + "enum": [ + "None", + "DolbyAtmos", + "DTSX" + ], + "type": "string", + "description": "An enum representing formats of spatial audio." + }, + "AuthenticateUserByName": { + "type": "object", + "properties": { + "Username": { + "type": "string", + "description": "Gets or sets the username.", + "nullable": true + }, + "Pw": { + "type": "string", + "description": "Gets or sets the plain text password.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "The authenticate user by name request body." + }, + "AuthenticationInfo": { + "type": "object", + "properties": { + "Id": { + "type": "integer", + "description": "Gets or sets the identifier.", + "format": "int64" + }, + "AccessToken": { + "type": "string", + "description": "Gets or sets the access token.", + "nullable": true + }, + "DeviceId": { + "type": "string", + "description": "Gets or sets the device identifier.", + "nullable": true + }, + "AppName": { + "type": "string", + "description": "Gets or sets the name of the application.", + "nullable": true + }, + "AppVersion": { + "type": "string", + "description": "Gets or sets the application version.", + "nullable": true + }, + "DeviceName": { + "type": "string", + "description": "Gets or sets the name of the device.", + "nullable": true + }, + "UserId": { + "type": "string", + "description": "Gets or sets the user identifier.", + "format": "uuid" + }, + "IsActive": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is active." + }, + "DateCreated": { + "type": "string", + "description": "Gets or sets the date created.", + "format": "date-time" + }, + "DateRevoked": { + "type": "string", + "description": "Gets or sets the date revoked.", + "format": "date-time", + "nullable": true + }, + "DateLastActivity": { + "type": "string", + "format": "date-time" + }, + "UserName": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "AuthenticationInfoQueryResult": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuthenticationInfo" + }, + "description": "Gets or sets the items." + }, + "TotalRecordCount": { + "type": "integer", + "description": "Gets or sets the total number of records available.", + "format": "int32" + }, + "StartIndex": { + "type": "integer", + "description": "Gets or sets the index of the first record in Items.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Query result container." + }, + "AuthenticationResult": { + "type": "object", + "properties": { + "User": { + "allOf": [ + { + "$ref": "#/components/schemas/UserDto" + } + ], + "description": "Class UserDto.", + "nullable": true + }, + "SessionInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/SessionInfoDto" + } + ], + "description": "Session info DTO.", + "nullable": true + }, + "AccessToken": { + "type": "string", + "description": "Gets or sets the access token.", + "nullable": true + }, + "ServerId": { + "type": "string", + "description": "Gets or sets the server id.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "A class representing an authentication result." + }, + "BackupManifestDto": { + "type": "object", + "properties": { + "ServerVersion": { + "type": "string", + "description": "Gets or sets the jellyfin version this backup was created with." + }, + "BackupEngineVersion": { + "type": "string", + "description": "Gets or sets the backup engine version this backup was created with." + }, + "DateCreated": { + "type": "string", + "description": "Gets or sets the date this backup was created with.", + "format": "date-time" + }, + "Path": { + "type": "string", + "description": "Gets or sets the path to the backup on the system." + }, + "Options": { + "allOf": [ + { + "$ref": "#/components/schemas/BackupOptionsDto" + } + ], + "description": "Gets or sets the contents of the backup archive." + } + }, + "additionalProperties": false, + "description": "Manifest type for backups internal structure." + }, + "BackupOptionsDto": { + "type": "object", + "properties": { + "Metadata": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the archive contains the Metadata contents." + }, + "Trickplay": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the archive contains the Trickplay contents." + }, + "Subtitles": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the archive contains the Subtitle contents." + }, + "Database": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the archive contains the Database contents." + } + }, + "additionalProperties": false, + "description": "Defines the optional contents of the backup archive." + }, + "BackupRestoreRequestDto": { + "type": "object", + "properties": { + "ArchiveFileName": { + "type": "string", + "description": "Gets or Sets the name of the backup archive to restore from. Must be present in MediaBrowser.Common.Configuration.IApplicationPaths.BackupPath." + } + }, + "additionalProperties": false, + "description": "Defines properties used to start a restore process." + }, + "BaseItemDto": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "nullable": true + }, + "ServerId": { + "type": "string", + "description": "Gets or sets the server identifier.", + "nullable": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the id.", + "format": "uuid" + }, + "Etag": { + "type": "string", + "description": "Gets or sets the etag.", + "nullable": true + }, + "SourceType": { + "type": "string", + "description": "Gets or sets the type of the source.", + "nullable": true + }, + "PlaylistItemId": { + "type": "string", + "description": "Gets or sets the playlist item identifier.", + "nullable": true + }, + "DateCreated": { + "type": "string", + "description": "Gets or sets the date created.", + "format": "date-time", + "nullable": true + }, + "DateLastMediaAdded": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "ExtraType": { + "enum": [ + "Unknown", + "Clip", + "Trailer", + "BehindTheScenes", + "DeletedScene", + "Interview", + "Scene", + "Sample", + "ThemeSong", + "ThemeVideo", + "Featurette", + "Short" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ExtraType" + } + ], + "nullable": true + }, + "AirsBeforeSeasonNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "AirsAfterSeasonNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "AirsBeforeEpisodeNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "CanDelete": { + "type": "boolean", + "nullable": true + }, + "CanDownload": { + "type": "boolean", + "nullable": true + }, + "HasLyrics": { + "type": "boolean", + "nullable": true + }, + "HasSubtitles": { + "type": "boolean", + "nullable": true + }, + "PreferredMetadataLanguage": { + "type": "string", + "nullable": true + }, + "PreferredMetadataCountryCode": { + "type": "string", + "nullable": true + }, + "Container": { + "type": "string", + "nullable": true + }, + "SortName": { + "type": "string", + "description": "Gets or sets the name of the sort.", + "nullable": true + }, + "ForcedSortName": { + "type": "string", + "nullable": true + }, + "Video3DFormat": { + "enum": [ + "HalfSideBySide", + "FullSideBySide", + "FullTopAndBottom", + "HalfTopAndBottom", + "MVC" + ], + "allOf": [ + { + "$ref": "#/components/schemas/Video3DFormat" + } + ], + "description": "Gets or sets the video3 D format.", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "description": "Gets or sets the premiere date.", + "format": "date-time", + "nullable": true + }, + "ExternalUrls": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalUrl" + }, + "description": "Gets or sets the external urls.", + "nullable": true + }, + "MediaSources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaSourceInfo" + }, + "description": "Gets or sets the media versions.", + "nullable": true + }, + "CriticRating": { + "type": "number", + "description": "Gets or sets the critic rating.", + "format": "float", + "nullable": true + }, + "ProductionLocations": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "EnableMediaSourceDisplay": { + "type": "boolean", + "nullable": true + }, + "OfficialRating": { + "type": "string", + "description": "Gets or sets the official rating.", + "nullable": true + }, + "CustomRating": { + "type": "string", + "description": "Gets or sets the custom rating.", + "nullable": true + }, + "ChannelId": { + "type": "string", + "description": "Gets or sets the channel identifier.", + "format": "uuid", + "nullable": true + }, + "ChannelName": { + "type": "string", + "nullable": true + }, + "Overview": { + "type": "string", + "description": "Gets or sets the overview.", + "nullable": true + }, + "Taglines": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the taglines.", + "nullable": true + }, + "Genres": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the genres.", + "nullable": true + }, + "CommunityRating": { + "type": "number", + "description": "Gets or sets the community rating.", + "format": "float", + "nullable": true + }, + "CumulativeRunTimeTicks": { + "type": "integer", + "description": "Gets or sets the cumulative run time ticks.", + "format": "int64", + "nullable": true + }, + "RunTimeTicks": { + "type": "integer", + "description": "Gets or sets the run time ticks.", + "format": "int64", + "nullable": true + }, + "PlayAccess": { + "enum": [ + "Full", + "None" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlayAccess" + } + ], + "description": "Gets or sets the play access.", + "nullable": true + }, + "AspectRatio": { + "type": "string", + "description": "Gets or sets the aspect ratio.", + "nullable": true + }, + "ProductionYear": { + "type": "integer", + "description": "Gets or sets the production year.", + "format": "int32", + "nullable": true + }, + "IsPlaceHolder": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is place holder.", + "nullable": true + }, + "Number": { + "type": "string", + "description": "Gets or sets the number.", + "nullable": true + }, + "ChannelNumber": { + "type": "string", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "description": "Gets or sets the index number.", + "format": "int32", + "nullable": true + }, + "IndexNumberEnd": { + "type": "integer", + "description": "Gets or sets the index number end.", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "description": "Gets or sets the parent index number.", + "format": "int32", + "nullable": true + }, + "RemoteTrailers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaUrl" + }, + "description": "Gets or sets the trailer urls.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "IsHD": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is HD.", + "nullable": true + }, + "IsFolder": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is folder.", + "nullable": true + }, + "ParentId": { + "type": "string", + "description": "Gets or sets the parent id.", + "format": "uuid", + "nullable": true + }, + "Type": { + "enum": [ + "AggregateFolder", + "Audio", + "AudioBook", + "BasePluginFolder", + "Book", + "BoxSet", + "Channel", + "ChannelFolderItem", + "CollectionFolder", + "Episode", + "Folder", + "Genre", + "ManualPlaylistsFolder", + "Movie", + "LiveTvChannel", + "LiveTvProgram", + "MusicAlbum", + "MusicArtist", + "MusicGenre", + "MusicVideo", + "Person", + "Photo", + "PhotoAlbum", + "Playlist", + "PlaylistsFolder", + "Program", + "Recording", + "Season", + "Series", + "Studio", + "Trailer", + "TvChannel", + "TvProgram", + "UserRootFolder", + "UserView", + "Video", + "Year" + ], + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemKind" + } + ], + "description": "Gets or sets the type." + }, + "People": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemPerson" + }, + "description": "Gets or sets the people.", + "nullable": true + }, + "Studios": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameGuidPair" + }, + "description": "Gets or sets the studios.", + "nullable": true + }, + "GenreItems": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameGuidPair" + }, + "nullable": true + }, + "ParentLogoItemId": { + "type": "string", + "description": "Gets or sets whether the item has a logo, this will hold the Id of the Parent that has one.", + "format": "uuid", + "nullable": true + }, + "ParentBackdropItemId": { + "type": "string", + "description": "Gets or sets whether the item has any backdrops, this will hold the Id of the Parent that has one.", + "format": "uuid", + "nullable": true + }, + "ParentBackdropImageTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the parent backdrop image tags.", + "nullable": true + }, + "LocalTrailerCount": { + "type": "integer", + "description": "Gets or sets the local trailer count.", + "format": "int32", + "nullable": true + }, + "UserData": { + "allOf": [ + { + "$ref": "#/components/schemas/UserItemDataDto" + } + ], + "description": "Gets or sets the user data for this item based on the user it's being requested for.", + "nullable": true + }, + "RecursiveItemCount": { + "type": "integer", + "description": "Gets or sets the recursive item count.", + "format": "int32", + "nullable": true + }, + "ChildCount": { + "type": "integer", + "description": "Gets or sets the child count.", + "format": "int32", + "nullable": true + }, + "SeriesName": { + "type": "string", + "description": "Gets or sets the name of the series.", + "nullable": true + }, + "SeriesId": { + "type": "string", + "description": "Gets or sets the series id.", + "format": "uuid", + "nullable": true + }, + "SeasonId": { + "type": "string", + "description": "Gets or sets the season identifier.", + "format": "uuid", + "nullable": true + }, + "SpecialFeatureCount": { + "type": "integer", + "description": "Gets or sets the special feature count.", + "format": "int32", + "nullable": true + }, + "DisplayPreferencesId": { + "type": "string", + "description": "Gets or sets the display preferences id.", + "nullable": true + }, + "Status": { + "type": "string", + "description": "Gets or sets the status.", + "nullable": true + }, + "AirTime": { + "type": "string", + "description": "Gets or sets the air time.", + "nullable": true + }, + "AirDays": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DayOfWeek" + }, + "description": "Gets or sets the air days.", + "nullable": true + }, + "Tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the tags.", + "nullable": true + }, + "PrimaryImageAspectRatio": { + "type": "number", + "description": "Gets or sets the primary image aspect ratio, after image enhancements.", + "format": "double", + "nullable": true + }, + "Artists": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the artists.", + "nullable": true + }, + "ArtistItems": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameGuidPair" + }, + "description": "Gets or sets the artist items.", + "nullable": true + }, + "Album": { + "type": "string", + "description": "Gets or sets the album.", + "nullable": true + }, + "CollectionType": { + "enum": [ + "unknown", + "movies", + "tvshows", + "music", + "musicvideos", + "trailers", + "homevideos", + "boxsets", + "books", + "photos", + "livetv", + "playlists", + "folders" + ], + "allOf": [ + { + "$ref": "#/components/schemas/CollectionType" + } + ], + "description": "Gets or sets the type of the collection.", + "nullable": true + }, + "DisplayOrder": { + "type": "string", + "description": "Gets or sets the display order.", + "nullable": true + }, + "AlbumId": { + "type": "string", + "description": "Gets or sets the album id.", + "format": "uuid", + "nullable": true + }, + "AlbumPrimaryImageTag": { + "type": "string", + "description": "Gets or sets the album image tag.", + "nullable": true + }, + "SeriesPrimaryImageTag": { + "type": "string", + "description": "Gets or sets the series primary image tag.", + "nullable": true + }, + "AlbumArtist": { + "type": "string", + "description": "Gets or sets the album artist.", + "nullable": true + }, + "AlbumArtists": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameGuidPair" + }, + "description": "Gets or sets the album artists.", + "nullable": true + }, + "SeasonName": { + "type": "string", + "description": "Gets or sets the name of the season.", + "nullable": true + }, + "MediaStreams": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaStream" + }, + "description": "Gets or sets the media streams.", + "nullable": true + }, + "VideoType": { + "enum": [ + "VideoFile", + "Iso", + "Dvd", + "BluRay" + ], + "allOf": [ + { + "$ref": "#/components/schemas/VideoType" + } + ], + "description": "Gets or sets the type of the video.", + "nullable": true + }, + "PartCount": { + "type": "integer", + "description": "Gets or sets the part count.", + "format": "int32", + "nullable": true + }, + "MediaSourceCount": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ImageTags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Gets or sets the image tags.", + "nullable": true + }, + "BackdropImageTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the backdrop image tags.", + "nullable": true + }, + "ScreenshotImageTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the screenshot image tags.", + "nullable": true + }, + "ParentLogoImageTag": { + "type": "string", + "description": "Gets or sets the parent logo image tag.", + "nullable": true + }, + "ParentArtItemId": { + "type": "string", + "description": "Gets or sets whether the item has fan art, this will hold the Id of the Parent that has one.", + "format": "uuid", + "nullable": true + }, + "ParentArtImageTag": { + "type": "string", + "description": "Gets or sets the parent art image tag.", + "nullable": true + }, + "SeriesThumbImageTag": { + "type": "string", + "description": "Gets or sets the series thumb image tag.", + "nullable": true + }, + "ImageBlurHashes": { + "type": "object", + "properties": { + "Primary": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Art": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Backdrop": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Banner": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Logo": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Thumb": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Disc": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Box": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Screenshot": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Menu": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Chapter": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "BoxRear": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Profile": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "description": "Gets or sets the blurhashes for the image tags.\r\nMaps image type to dictionary mapping image tag to blurhash value.", + "nullable": true + }, + "SeriesStudio": { + "type": "string", + "description": "Gets or sets the series studio.", + "nullable": true + }, + "ParentThumbItemId": { + "type": "string", + "description": "Gets or sets the parent thumb item id.", + "format": "uuid", + "nullable": true + }, + "ParentThumbImageTag": { + "type": "string", + "description": "Gets or sets the parent thumb image tag.", + "nullable": true + }, + "ParentPrimaryImageItemId": { + "type": "string", + "description": "Gets or sets the parent primary image item identifier.", + "format": "uuid", + "nullable": true + }, + "ParentPrimaryImageTag": { + "type": "string", + "description": "Gets or sets the parent primary image tag.", + "nullable": true + }, + "Chapters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChapterInfo" + }, + "description": "Gets or sets the chapters.", + "nullable": true + }, + "Trickplay": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/TrickplayInfoDto" + } + }, + "description": "Gets or sets the trickplay manifest.", + "nullable": true + }, + "LocationType": { + "enum": [ + "FileSystem", + "Remote", + "Virtual", + "Offline" + ], + "allOf": [ + { + "$ref": "#/components/schemas/LocationType" + } + ], + "description": "Gets or sets the type of the location.", + "nullable": true + }, + "IsoType": { + "enum": [ + "Dvd", + "BluRay" + ], + "allOf": [ + { + "$ref": "#/components/schemas/IsoType" + } + ], + "description": "Gets or sets the type of the iso.", + "nullable": true + }, + "MediaType": { + "enum": [ + "Unknown", + "Video", + "Audio", + "Photo", + "Book" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaType" + } + ], + "description": "Gets or sets the type of the media.", + "default": "Unknown" + }, + "EndDate": { + "type": "string", + "description": "Gets or sets the end date.", + "format": "date-time", + "nullable": true + }, + "LockedFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataField" + }, + "description": "Gets or sets the locked fields.", + "nullable": true + }, + "TrailerCount": { + "type": "integer", + "description": "Gets or sets the trailer count.", + "format": "int32", + "nullable": true + }, + "MovieCount": { + "type": "integer", + "description": "Gets or sets the movie count.", + "format": "int32", + "nullable": true + }, + "SeriesCount": { + "type": "integer", + "description": "Gets or sets the series count.", + "format": "int32", + "nullable": true + }, + "ProgramCount": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "EpisodeCount": { + "type": "integer", + "description": "Gets or sets the episode count.", + "format": "int32", + "nullable": true + }, + "SongCount": { + "type": "integer", + "description": "Gets or sets the song count.", + "format": "int32", + "nullable": true + }, + "AlbumCount": { + "type": "integer", + "description": "Gets or sets the album count.", + "format": "int32", + "nullable": true + }, + "ArtistCount": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "MusicVideoCount": { + "type": "integer", + "description": "Gets or sets the music video count.", + "format": "int32", + "nullable": true + }, + "LockData": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [enable internet providers].", + "nullable": true + }, + "Width": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "Height": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "CameraMake": { + "type": "string", + "nullable": true + }, + "CameraModel": { + "type": "string", + "nullable": true + }, + "Software": { + "type": "string", + "nullable": true + }, + "ExposureTime": { + "type": "number", + "format": "double", + "nullable": true + }, + "FocalLength": { + "type": "number", + "format": "double", + "nullable": true + }, + "ImageOrientation": { + "enum": [ + "TopLeft", + "TopRight", + "BottomRight", + "BottomLeft", + "LeftTop", + "RightTop", + "RightBottom", + "LeftBottom" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageOrientation" + } + ], + "nullable": true + }, + "Aperture": { + "type": "number", + "format": "double", + "nullable": true + }, + "ShutterSpeed": { + "type": "number", + "format": "double", + "nullable": true + }, + "Latitude": { + "type": "number", + "format": "double", + "nullable": true + }, + "Longitude": { + "type": "number", + "format": "double", + "nullable": true + }, + "Altitude": { + "type": "number", + "format": "double", + "nullable": true + }, + "IsoSpeedRating": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "SeriesTimerId": { + "type": "string", + "description": "Gets or sets the series timer identifier.", + "nullable": true + }, + "ProgramId": { + "type": "string", + "description": "Gets or sets the program identifier.", + "nullable": true + }, + "ChannelPrimaryImageTag": { + "type": "string", + "description": "Gets or sets the channel primary image tag.", + "nullable": true + }, + "StartDate": { + "type": "string", + "description": "Gets or sets the start date of the recording, in UTC.", + "format": "date-time", + "nullable": true + }, + "CompletionPercentage": { + "type": "number", + "description": "Gets or sets the completion percentage.", + "format": "double", + "nullable": true + }, + "IsRepeat": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is repeat.", + "nullable": true + }, + "EpisodeTitle": { + "type": "string", + "description": "Gets or sets the episode title.", + "nullable": true + }, + "ChannelType": { + "enum": [ + "TV", + "Radio" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelType" + } + ], + "description": "Gets or sets the type of the channel.", + "nullable": true + }, + "Audio": { + "enum": [ + "Mono", + "Stereo", + "Dolby", + "DolbyDigital", + "Thx", + "Atmos" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ProgramAudio" + } + ], + "description": "Gets or sets the audio.", + "nullable": true + }, + "IsMovie": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is movie.", + "nullable": true + }, + "IsSports": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is sports.", + "nullable": true + }, + "IsSeries": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is series.", + "nullable": true + }, + "IsLive": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is live.", + "nullable": true + }, + "IsNews": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is news.", + "nullable": true + }, + "IsKids": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is kids.", + "nullable": true + }, + "IsPremiere": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is premiere.", + "nullable": true + }, + "TimerId": { + "type": "string", + "description": "Gets or sets the timer identifier.", + "nullable": true + }, + "NormalizationGain": { + "type": "number", + "description": "Gets or sets the gain required for audio normalization.", + "format": "float", + "nullable": true + }, + "CurrentProgram": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemDto" + } + ], + "description": "Gets or sets the current program.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "This is strictly used as a data transfer object from the api layer.\r\nThis holds information about a BaseItem in a format that is convenient for the client." + }, + "BaseItemDtoQueryResult": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + }, + "description": "Gets or sets the items." + }, + "TotalRecordCount": { + "type": "integer", + "description": "Gets or sets the total number of records available.", + "format": "int32" + }, + "StartIndex": { + "type": "integer", + "description": "Gets or sets the index of the first record in Items.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Query result container." + }, + "BaseItemKind": { + "enum": [ + "AggregateFolder", + "Audio", + "AudioBook", + "BasePluginFolder", + "Book", + "BoxSet", + "Channel", + "ChannelFolderItem", + "CollectionFolder", + "Episode", + "Folder", + "Genre", + "ManualPlaylistsFolder", + "Movie", + "LiveTvChannel", + "LiveTvProgram", + "MusicAlbum", + "MusicArtist", + "MusicGenre", + "MusicVideo", + "Person", + "Photo", + "PhotoAlbum", + "Playlist", + "PlaylistsFolder", + "Program", + "Recording", + "Season", + "Series", + "Studio", + "Trailer", + "TvChannel", + "TvProgram", + "UserRootFolder", + "UserView", + "Video", + "Year" + ], + "type": "string", + "description": "The base item kind." + }, + "BaseItemPerson": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the identifier.", + "format": "uuid" + }, + "Role": { + "type": "string", + "description": "Gets or sets the role.", + "nullable": true + }, + "Type": { + "enum": [ + "Unknown", + "Actor", + "Director", + "Composer", + "Writer", + "GuestStar", + "Producer", + "Conductor", + "Lyricist", + "Arranger", + "Engineer", + "Mixer", + "Remixer", + "Creator", + "Artist", + "AlbumArtist", + "Author", + "Illustrator", + "Penciller", + "Inker", + "Colorist", + "Letterer", + "CoverArtist", + "Editor", + "Translator" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PersonKind" + } + ], + "description": "Gets or sets the type.", + "default": "Unknown" + }, + "PrimaryImageTag": { + "type": "string", + "description": "Gets or sets the primary image tag.", + "nullable": true + }, + "ImageBlurHashes": { + "type": "object", + "properties": { + "Primary": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Art": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Backdrop": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Banner": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Logo": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Thumb": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Disc": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Box": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Screenshot": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Menu": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Chapter": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "BoxRear": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Profile": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "description": "Gets or sets the primary image blurhash.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "This is used by the api to get information about a Person within a BaseItem." + }, + "BasePluginConfiguration": { + "type": "object", + "additionalProperties": false, + "description": "Class BasePluginConfiguration." + }, + "BookInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "description": "Gets or sets the original title.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "MetadataLanguage": { + "type": "string", + "description": "Gets or sets the metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "Year": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "IsAutomated": { + "type": "boolean" + }, + "SeriesName": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "BookInfoRemoteSearchQuery": { + "type": "object", + "properties": { + "SearchInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/BookInfo" + } + ], + "nullable": true + }, + "ItemId": { + "type": "string", + "format": "uuid" + }, + "SearchProviderName": { + "type": "string", + "description": "Gets or sets the provider name to search within if set.", + "nullable": true + }, + "IncludeDisabledProviders": { + "type": "boolean", + "description": "Gets or sets a value indicating whether disabled providers should be included." + } + }, + "additionalProperties": false + }, + "BoxSetInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "description": "Gets or sets the original title.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "MetadataLanguage": { + "type": "string", + "description": "Gets or sets the metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "Year": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "IsAutomated": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "BoxSetInfoRemoteSearchQuery": { + "type": "object", + "properties": { + "SearchInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/BoxSetInfo" + } + ], + "nullable": true + }, + "ItemId": { + "type": "string", + "format": "uuid" + }, + "SearchProviderName": { + "type": "string", + "description": "Gets or sets the provider name to search within if set.", + "nullable": true + }, + "IncludeDisabledProviders": { + "type": "boolean", + "description": "Gets or sets a value indicating whether disabled providers should be included." + } + }, + "additionalProperties": false + }, + "BrandingOptionsDto": { + "type": "object", + "properties": { + "LoginDisclaimer": { + "type": "string", + "description": "Gets or sets the login disclaimer.", + "nullable": true + }, + "CustomCss": { + "type": "string", + "description": "Gets or sets the custom CSS.", + "nullable": true + }, + "SplashscreenEnabled": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to enable the splashscreen." + } + }, + "additionalProperties": false, + "description": "The branding options DTO for API use.\r\nThis DTO excludes SplashscreenLocation to prevent it from being updated via API." + }, + "BufferRequestDto": { + "type": "object", + "properties": { + "When": { + "type": "string", + "description": "Gets or sets when the request has been made by the client.", + "format": "date-time" + }, + "PositionTicks": { + "type": "integer", + "description": "Gets or sets the position ticks.", + "format": "int64" + }, + "IsPlaying": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the client playback is unpaused." + }, + "PlaylistItemId": { + "type": "string", + "description": "Gets or sets the playlist item identifier of the playing item.", + "format": "uuid" + } + }, + "additionalProperties": false, + "description": "Class BufferRequestDto." + }, + "CastReceiverApplication": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "description": "Gets or sets the cast receiver application id." + }, + "Name": { + "type": "string", + "description": "Gets or sets the cast receiver application name." + } + }, + "additionalProperties": false, + "description": "The cast receiver application model." + }, + "ChannelFeatures": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name." + }, + "Id": { + "type": "string", + "description": "Gets or sets the identifier.", + "format": "uuid" + }, + "CanSearch": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance can search." + }, + "MediaTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelMediaType" + }, + "description": "Gets or sets the media types." + }, + "ContentTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelMediaContentType" + }, + "description": "Gets or sets the content types." + }, + "MaxPageSize": { + "type": "integer", + "description": "Gets or sets the maximum number of records the channel allows retrieving at a time.", + "format": "int32", + "nullable": true + }, + "AutoRefreshLevels": { + "type": "integer", + "description": "Gets or sets the automatic refresh levels.", + "format": "int32", + "nullable": true + }, + "DefaultSortFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelItemSortField" + }, + "description": "Gets or sets the default sort orders." + }, + "SupportsSortOrderToggle": { + "type": "boolean", + "description": "Gets or sets a value indicating whether a sort ascending/descending toggle is supported." + }, + "SupportsLatestMedia": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [supports latest media]." + }, + "CanFilter": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance can filter." + }, + "SupportsContentDownloading": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [supports content downloading]." + } + }, + "additionalProperties": false + }, + "ChannelItemSortField": { + "enum": [ + "Name", + "CommunityRating", + "PremiereDate", + "DateCreated", + "Runtime", + "PlayCount", + "CommunityPlayCount" + ], + "type": "string" + }, + "ChannelMappingOptionsDto": { + "type": "object", + "properties": { + "TunerChannels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TunerChannelMapping" + }, + "description": "Gets or sets list of tuner channels." + }, + "ProviderChannels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameIdPair" + }, + "description": "Gets or sets list of provider channels." + }, + "Mappings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameValuePair" + }, + "description": "Gets or sets list of mappings." + }, + "ProviderName": { + "type": "string", + "description": "Gets or sets provider name.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Channel mapping options dto." + }, + "ChannelMediaContentType": { + "enum": [ + "Clip", + "Podcast", + "Trailer", + "Movie", + "Episode", + "Song", + "MovieExtra", + "TvExtra" + ], + "type": "string" + }, + "ChannelMediaType": { + "enum": [ + "Audio", + "Video", + "Photo" + ], + "type": "string" + }, + "ChannelType": { + "enum": [ + "TV", + "Radio" + ], + "type": "string", + "description": "Enum ChannelType." + }, + "ChapterInfo": { + "type": "object", + "properties": { + "StartPositionTicks": { + "type": "integer", + "description": "Gets or sets the start position ticks.", + "format": "int64" + }, + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "ImagePath": { + "type": "string", + "description": "Gets or sets the image path.", + "nullable": true + }, + "ImageDateModified": { + "type": "string", + "format": "date-time" + }, + "ImageTag": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class ChapterInfo." + }, + "ClientCapabilitiesDto": { + "type": "object", + "properties": { + "PlayableMediaTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + }, + "description": "Gets or sets the list of playable media types." + }, + "SupportedCommands": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GeneralCommandType" + }, + "description": "Gets or sets the list of supported commands." + }, + "SupportsMediaControl": { + "type": "boolean", + "description": "Gets or sets a value indicating whether session supports media control." + }, + "SupportsPersistentIdentifier": { + "type": "boolean", + "description": "Gets or sets a value indicating whether session supports a persistent identifier." + }, + "DeviceProfile": { + "allOf": [ + { + "$ref": "#/components/schemas/DeviceProfile" + } + ], + "description": "Gets or sets the device profile.", + "nullable": true + }, + "AppStoreUrl": { + "type": "string", + "description": "Gets or sets the app store url.", + "nullable": true + }, + "IconUrl": { + "type": "string", + "description": "Gets or sets the icon url.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Client capabilities dto." + }, + "ClientLogDocumentResponseDto": { + "type": "object", + "properties": { + "FileName": { + "type": "string", + "description": "Gets the resulting filename." + } + }, + "additionalProperties": false, + "description": "Client log document response dto." + }, + "CodecProfile": { + "type": "object", + "properties": { + "Type": { + "enum": [ + "Video", + "VideoAudio", + "Audio" + ], + "allOf": [ + { + "$ref": "#/components/schemas/CodecType" + } + ], + "description": "Gets or sets the MediaBrowser.Model.Dlna.CodecType which this container must meet." + }, + "Conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProfileCondition" + }, + "description": "Gets or sets the list of MediaBrowser.Model.Dlna.ProfileCondition which this profile must meet." + }, + "ApplyConditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProfileCondition" + }, + "description": "Gets or sets the list of MediaBrowser.Model.Dlna.ProfileCondition to apply if this profile is met." + }, + "Codec": { + "type": "string", + "description": "Gets or sets the codec(s) that this profile applies to.", + "nullable": true + }, + "Container": { + "type": "string", + "description": "Gets or sets the container(s) which this profile will be applied to.", + "nullable": true + }, + "SubContainer": { + "type": "string", + "description": "Gets or sets the sub-container(s) which this profile will be applied to.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Defines the MediaBrowser.Model.Dlna.CodecProfile." + }, + "CodecType": { + "enum": [ + "Video", + "VideoAudio", + "Audio" + ], + "type": "string" + }, + "CollectionCreationResult": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, + "CollectionType": { + "enum": [ + "unknown", + "movies", + "tvshows", + "music", + "musicvideos", + "trailers", + "homevideos", + "boxsets", + "books", + "photos", + "livetv", + "playlists", + "folders" + ], + "type": "string", + "description": "Collection type." + }, + "CollectionTypeOptions": { + "enum": [ + "movies", + "tvshows", + "music", + "musicvideos", + "homevideos", + "boxsets", + "books", + "mixed" + ], + "type": "string", + "description": "The collection type options." + }, + "ConfigImageTypes": { + "type": "object", + "properties": { + "BackdropSizes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "BaseUrl": { + "type": "string", + "nullable": true + }, + "LogoSizes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "PosterSizes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "ProfileSizes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "SecureBaseUrl": { + "type": "string", + "nullable": true + }, + "StillSizes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "ConfigurationPageInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name." + }, + "EnableInMainMenu": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the configurations page is enabled in the main menu." + }, + "MenuSection": { + "type": "string", + "description": "Gets or sets the menu section.", + "nullable": true + }, + "MenuIcon": { + "type": "string", + "description": "Gets or sets the menu icon.", + "nullable": true + }, + "DisplayName": { + "type": "string", + "description": "Gets or sets the display name.", + "nullable": true + }, + "PluginId": { + "type": "string", + "description": "Gets or sets the plugin id.", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false, + "description": "The configuration page info." + }, + "ContainerProfile": { + "type": "object", + "properties": { + "Type": { + "enum": [ + "Audio", + "Video", + "Photo", + "Subtitle", + "Lyric" + ], + "allOf": [ + { + "$ref": "#/components/schemas/DlnaProfileType" + } + ], + "description": "Gets or sets the MediaBrowser.Model.Dlna.DlnaProfileType which this container must meet." + }, + "Conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProfileCondition" + }, + "description": "Gets or sets the list of MediaBrowser.Model.Dlna.ProfileCondition which this container will be applied to." + }, + "Container": { + "type": "string", + "description": "Gets or sets the container(s) which this container must meet.", + "nullable": true + }, + "SubContainer": { + "type": "string", + "description": "Gets or sets the sub container(s) which this container must meet.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Defines the MediaBrowser.Model.Dlna.ContainerProfile." + }, + "CountryInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "DisplayName": { + "type": "string", + "description": "Gets or sets the display name.", + "nullable": true + }, + "TwoLetterISORegionName": { + "type": "string", + "description": "Gets or sets the name of the two letter ISO region.", + "nullable": true + }, + "ThreeLetterISORegionName": { + "type": "string", + "description": "Gets or sets the name of the three letter ISO region.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class CountryInfo." + }, + "CreatePlaylistDto": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name of the new playlist." + }, + "Ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Gets or sets item ids to add to the playlist." + }, + "UserId": { + "type": "string", + "description": "Gets or sets the user id.", + "format": "uuid", + "nullable": true + }, + "MediaType": { + "enum": [ + "Unknown", + "Video", + "Audio", + "Photo", + "Book" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaType" + } + ], + "description": "Gets or sets the media type.", + "nullable": true + }, + "Users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlaylistUserPermissions" + }, + "description": "Gets or sets the playlist users." + }, + "IsPublic": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the playlist is public." + } + }, + "additionalProperties": false, + "description": "Create new playlist dto." + }, + "CreateUserByName": { + "required": [ + "Name" + ], + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the username." + }, + "Password": { + "type": "string", + "description": "Gets or sets the password.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "The create user by name request body." + }, + "CultureDto": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets the name." + }, + "DisplayName": { + "type": "string", + "description": "Gets the display name." + }, + "TwoLetterISOLanguageName": { + "type": "string", + "description": "Gets the name of the two letter ISO language." + }, + "ThreeLetterISOLanguageName": { + "type": "string", + "description": "Gets the name of the three letter ISO language.", + "nullable": true, + "readOnly": true + }, + "ThreeLetterISOLanguageNames": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "description": "Class CultureDto." + }, + "CustomDatabaseOption": { + "type": "object", + "properties": { + "Key": { + "type": "string", + "description": "Gets or sets the key of the value." + }, + "Value": { + "type": "string", + "description": "Gets or sets the value." + } + }, + "additionalProperties": false, + "description": "The custom value option for custom database providers." + }, + "CustomDatabaseOptions": { + "type": "object", + "properties": { + "PluginName": { + "type": "string", + "description": "Gets or sets the Plugin name to search for database providers." + }, + "PluginAssembly": { + "type": "string", + "description": "Gets or sets the plugin assembly to search for providers." + }, + "ConnectionString": { + "type": "string", + "description": "Gets or sets the connection string for the custom database provider." + }, + "Options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomDatabaseOption" + }, + "description": "Gets or sets the list of extra options for the custom provider." + } + }, + "additionalProperties": false, + "description": "Defines the options for a custom database connector." + }, + "DatabaseConfigurationOptions": { + "type": "object", + "properties": { + "DatabaseType": { + "type": "string", + "description": "Gets or Sets the type of database jellyfin should use." + }, + "CustomProviderOptions": { + "allOf": [ + { + "$ref": "#/components/schemas/CustomDatabaseOptions" + } + ], + "description": "Gets or sets the options required to use a custom database provider.", + "nullable": true + }, + "LockingBehavior": { + "enum": [ + "NoLock", + "Pessimistic", + "Optimistic" + ], + "allOf": [ + { + "$ref": "#/components/schemas/DatabaseLockingBehaviorTypes" + } + ], + "description": "Gets or Sets the kind of locking behavior jellyfin should perform. Possible options are \"NoLock\", \"Pessimistic\", \"Optimistic\".\r\nDefaults to \"NoLock\"." + } + }, + "additionalProperties": false, + "description": "Options to configure jellyfins managed database." + }, + "DatabaseLockingBehaviorTypes": { + "enum": [ + "NoLock", + "Pessimistic", + "Optimistic" + ], + "type": "string", + "description": "Defines all possible methods for locking database access for concurrent queries." + }, + "DayOfWeek": { + "enum": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "type": "string" + }, + "DayPattern": { + "enum": [ + "Daily", + "Weekdays", + "Weekends" + ], + "type": "string" + }, + "DefaultDirectoryBrowserInfoDto": { + "type": "object", + "properties": { + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Default directory browser info." + }, + "DeinterlaceMethod": { + "enum": [ + "yadif", + "bwdif" + ], + "type": "string", + "description": "Enum containing deinterlace methods." + }, + "DeviceInfoDto": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "CustomName": { + "type": "string", + "description": "Gets or sets the custom name.", + "nullable": true + }, + "AccessToken": { + "type": "string", + "description": "Gets or sets the access token.", + "nullable": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the identifier.", + "nullable": true + }, + "LastUserName": { + "type": "string", + "description": "Gets or sets the last name of the user.", + "nullable": true + }, + "AppName": { + "type": "string", + "description": "Gets or sets the name of the application.", + "nullable": true + }, + "AppVersion": { + "type": "string", + "description": "Gets or sets the application version.", + "nullable": true + }, + "LastUserId": { + "type": "string", + "description": "Gets or sets the last user identifier.", + "format": "uuid", + "nullable": true + }, + "DateLastActivity": { + "type": "string", + "description": "Gets or sets the date last modified.", + "format": "date-time", + "nullable": true + }, + "Capabilities": { + "allOf": [ + { + "$ref": "#/components/schemas/ClientCapabilitiesDto" + } + ], + "description": "Gets or sets the capabilities." + }, + "IconUrl": { + "type": "string", + "description": "Gets or sets the icon URL.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "A DTO representing device information." + }, + "DeviceInfoDtoQueryResult": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeviceInfoDto" + }, + "description": "Gets or sets the items." + }, + "TotalRecordCount": { + "type": "integer", + "description": "Gets or sets the total number of records available.", + "format": "int32" + }, + "StartIndex": { + "type": "integer", + "description": "Gets or sets the index of the first record in Items.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Query result container." + }, + "DeviceOptionsDto": { + "type": "object", + "properties": { + "Id": { + "type": "integer", + "description": "Gets or sets the id.", + "format": "int32" + }, + "DeviceId": { + "type": "string", + "description": "Gets or sets the device id.", + "nullable": true + }, + "CustomName": { + "type": "string", + "description": "Gets or sets the custom name.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "A dto representing custom options for a device." + }, + "DeviceProfile": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name of this device profile. User profiles must have a unique name.", + "nullable": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the unique internal identifier.", + "format": "uuid", + "nullable": true + }, + "MaxStreamingBitrate": { + "type": "integer", + "description": "Gets or sets the maximum allowed bitrate for all streamed content.", + "format": "int32", + "nullable": true + }, + "MaxStaticBitrate": { + "type": "integer", + "description": "Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files).", + "format": "int32", + "nullable": true + }, + "MusicStreamingTranscodingBitrate": { + "type": "integer", + "description": "Gets or sets the maximum allowed bitrate for transcoded music streams.", + "format": "int32", + "nullable": true + }, + "MaxStaticMusicBitrate": { + "type": "integer", + "description": "Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files.", + "format": "int32", + "nullable": true + }, + "DirectPlayProfiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DirectPlayProfile" + }, + "description": "Gets or sets the direct play profiles." + }, + "TranscodingProfiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TranscodingProfile" + }, + "description": "Gets or sets the transcoding profiles." + }, + "ContainerProfiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContainerProfile" + }, + "description": "Gets or sets the container profiles. Failing to meet these optional conditions causes transcoding to occur." + }, + "CodecProfiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CodecProfile" + }, + "description": "Gets or sets the codec profiles." + }, + "SubtitleProfiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SubtitleProfile" + }, + "description": "Gets or sets the subtitle profiles." + } + }, + "additionalProperties": false, + "description": "A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.\r\n
\r\nSpecifically, it defines the supported containers and\r\ncodecs (video and/or audio, including codec profiles and levels)\r\nthe device is able to direct play (without transcoding or remuxing),\r\nas well as which containers/codecs to transcode to in case it isn't." + }, + "DirectPlayProfile": { + "type": "object", + "properties": { + "Container": { + "type": "string", + "description": "Gets or sets the container." + }, + "AudioCodec": { + "type": "string", + "description": "Gets or sets the audio codec.", + "nullable": true + }, + "VideoCodec": { + "type": "string", + "description": "Gets or sets the video codec.", + "nullable": true + }, + "Type": { + "enum": [ + "Audio", + "Video", + "Photo", + "Subtitle", + "Lyric" + ], + "allOf": [ + { + "$ref": "#/components/schemas/DlnaProfileType" + } + ], + "description": "Gets or sets the Dlna profile type." + } + }, + "additionalProperties": false, + "description": "Defines the MediaBrowser.Model.Dlna.DirectPlayProfile." + }, + "DisplayPreferencesDto": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "description": "Gets or sets the user id.", + "nullable": true + }, + "ViewType": { + "type": "string", + "description": "Gets or sets the type of the view.", + "nullable": true + }, + "SortBy": { + "type": "string", + "description": "Gets or sets the sort by.", + "nullable": true + }, + "IndexBy": { + "type": "string", + "description": "Gets or sets the index by.", + "nullable": true + }, + "RememberIndexing": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [remember indexing]." + }, + "PrimaryImageHeight": { + "type": "integer", + "description": "Gets or sets the height of the primary image.", + "format": "int32" + }, + "PrimaryImageWidth": { + "type": "integer", + "description": "Gets or sets the width of the primary image.", + "format": "int32" + }, + "CustomPrefs": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the custom prefs." + }, + "ScrollDirection": { + "enum": [ + "Horizontal", + "Vertical" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ScrollDirection" + } + ], + "description": "Gets or sets the scroll direction." + }, + "ShowBackdrop": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to show backdrops on this item." + }, + "RememberSorting": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [remember sorting]." + }, + "SortOrder": { + "enum": [ + "Ascending", + "Descending" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SortOrder" + } + ], + "description": "Gets or sets the sort order." + }, + "ShowSidebar": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [show sidebar]." + }, + "Client": { + "type": "string", + "description": "Gets or sets the client.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Defines the display preferences for any item that supports them (usually Folders)." + }, + "DlnaProfileType": { + "enum": [ + "Audio", + "Video", + "Photo", + "Subtitle", + "Lyric" + ], + "type": "string" + }, + "DownMixStereoAlgorithms": { + "enum": [ + "None", + "Dave750", + "NightmodeDialogue", + "Rfc7845", + "Ac4" + ], + "type": "string", + "description": "An enum representing an algorithm to downmix surround sound to stereo." + }, + "DynamicDayOfWeek": { + "enum": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Everyday", + "Weekday", + "Weekend" + ], + "type": "string", + "description": "An enum that represents a day of the week, weekdays, weekends, or all days." + }, + "EmbeddedSubtitleOptions": { + "enum": [ + "AllowAll", + "AllowText", + "AllowImage", + "AllowNone" + ], + "type": "string", + "description": "An enum representing the options to disable embedded subs." + }, + "EncoderPreset": { + "enum": [ + "auto", + "placebo", + "veryslow", + "slower", + "slow", + "medium", + "fast", + "faster", + "veryfast", + "superfast", + "ultrafast" + ], + "type": "string", + "description": "Enum containing encoder presets." + }, + "EncodingContext": { + "enum": [ + "Streaming", + "Static" + ], + "type": "string" + }, + "EncodingOptions": { + "type": "object", + "properties": { + "EncodingThreadCount": { + "type": "integer", + "description": "Gets or sets the thread count used for encoding.", + "format": "int32" + }, + "TranscodingTempPath": { + "type": "string", + "description": "Gets or sets the temporary transcoding path.", + "nullable": true + }, + "FallbackFontPath": { + "type": "string", + "description": "Gets or sets the path to the fallback font.", + "nullable": true + }, + "EnableFallbackFont": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to use the fallback font." + }, + "EnableAudioVbr": { + "type": "boolean", + "description": "Gets or sets a value indicating whether audio VBR is enabled." + }, + "DownMixAudioBoost": { + "type": "number", + "description": "Gets or sets the audio boost applied when downmixing audio.", + "format": "double" + }, + "DownMixStereoAlgorithm": { + "enum": [ + "None", + "Dave750", + "NightmodeDialogue", + "Rfc7845", + "Ac4" + ], + "allOf": [ + { + "$ref": "#/components/schemas/DownMixStereoAlgorithms" + } + ], + "description": "Gets or sets the algorithm used for downmixing audio to stereo." + }, + "MaxMuxingQueueSize": { + "type": "integer", + "description": "Gets or sets the maximum size of the muxing queue.", + "format": "int32" + }, + "EnableThrottling": { + "type": "boolean", + "description": "Gets or sets a value indicating whether throttling is enabled." + }, + "ThrottleDelaySeconds": { + "type": "integer", + "description": "Gets or sets the delay after which throttling happens.", + "format": "int32" + }, + "EnableSegmentDeletion": { + "type": "boolean", + "description": "Gets or sets a value indicating whether segment deletion is enabled." + }, + "SegmentKeepSeconds": { + "type": "integer", + "description": "Gets or sets seconds for which segments should be kept before being deleted.", + "format": "int32" + }, + "HardwareAccelerationType": { + "enum": [ + "none", + "amf", + "qsv", + "nvenc", + "v4l2m2m", + "vaapi", + "videotoolbox", + "rkmpp" + ], + "allOf": [ + { + "$ref": "#/components/schemas/HardwareAccelerationType" + } + ], + "description": "Gets or sets the hardware acceleration type." + }, + "EncoderAppPath": { + "type": "string", + "description": "Gets or sets the FFmpeg path as set by the user via the UI.", + "nullable": true + }, + "EncoderAppPathDisplay": { + "type": "string", + "description": "Gets or sets the current FFmpeg path being used by the system and displayed on the transcode page.", + "nullable": true + }, + "VaapiDevice": { + "type": "string", + "description": "Gets or sets the VA-API device.", + "nullable": true + }, + "QsvDevice": { + "type": "string", + "description": "Gets or sets the QSV device.", + "nullable": true + }, + "EnableTonemapping": { + "type": "boolean", + "description": "Gets or sets a value indicating whether tonemapping is enabled." + }, + "EnableVppTonemapping": { + "type": "boolean", + "description": "Gets or sets a value indicating whether VPP tonemapping is enabled." + }, + "EnableVideoToolboxTonemapping": { + "type": "boolean", + "description": "Gets or sets a value indicating whether videotoolbox tonemapping is enabled." + }, + "TonemappingAlgorithm": { + "enum": [ + "none", + "clip", + "linear", + "gamma", + "reinhard", + "hable", + "mobius", + "bt2390" + ], + "allOf": [ + { + "$ref": "#/components/schemas/TonemappingAlgorithm" + } + ], + "description": "Gets or sets the tone-mapping algorithm." + }, + "TonemappingMode": { + "enum": [ + "auto", + "max", + "rgb", + "lum", + "itp" + ], + "allOf": [ + { + "$ref": "#/components/schemas/TonemappingMode" + } + ], + "description": "Gets or sets the tone-mapping mode." + }, + "TonemappingRange": { + "enum": [ + "auto", + "tv", + "pc" + ], + "allOf": [ + { + "$ref": "#/components/schemas/TonemappingRange" + } + ], + "description": "Gets or sets the tone-mapping range." + }, + "TonemappingDesat": { + "type": "number", + "description": "Gets or sets the tone-mapping desaturation.", + "format": "double" + }, + "TonemappingPeak": { + "type": "number", + "description": "Gets or sets the tone-mapping peak.", + "format": "double" + }, + "TonemappingParam": { + "type": "number", + "description": "Gets or sets the tone-mapping parameters.", + "format": "double" + }, + "VppTonemappingBrightness": { + "type": "number", + "description": "Gets or sets the VPP tone-mapping brightness.", + "format": "double" + }, + "VppTonemappingContrast": { + "type": "number", + "description": "Gets or sets the VPP tone-mapping contrast.", + "format": "double" + }, + "H264Crf": { + "type": "integer", + "description": "Gets or sets the H264 CRF.", + "format": "int32" + }, + "H265Crf": { + "type": "integer", + "description": "Gets or sets the H265 CRF.", + "format": "int32" + }, + "EncoderPreset": { + "enum": [ + "auto", + "placebo", + "veryslow", + "slower", + "slow", + "medium", + "fast", + "faster", + "veryfast", + "superfast", + "ultrafast" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncoderPreset" + } + ], + "description": "Gets or sets the encoder preset.", + "nullable": true + }, + "DeinterlaceDoubleRate": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the framerate is doubled when deinterlacing." + }, + "DeinterlaceMethod": { + "enum": [ + "yadif", + "bwdif" + ], + "allOf": [ + { + "$ref": "#/components/schemas/DeinterlaceMethod" + } + ], + "description": "Gets or sets the deinterlace method." + }, + "EnableDecodingColorDepth10Hevc": { + "type": "boolean", + "description": "Gets or sets a value indicating whether 10bit HEVC decoding is enabled." + }, + "EnableDecodingColorDepth10Vp9": { + "type": "boolean", + "description": "Gets or sets a value indicating whether 10bit VP9 decoding is enabled." + }, + "EnableDecodingColorDepth10HevcRext": { + "type": "boolean", + "description": "Gets or sets a value indicating whether 8/10bit HEVC RExt decoding is enabled." + }, + "EnableDecodingColorDepth12HevcRext": { + "type": "boolean", + "description": "Gets or sets a value indicating whether 12bit HEVC RExt decoding is enabled." + }, + "EnableEnhancedNvdecDecoder": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the enhanced NVDEC is enabled." + }, + "PreferSystemNativeHwDecoder": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the system native hardware decoder should be used." + }, + "EnableIntelLowPowerH264HwEncoder": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the Intel H264 low-power hardware encoder should be used." + }, + "EnableIntelLowPowerHevcHwEncoder": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the Intel HEVC low-power hardware encoder should be used." + }, + "EnableHardwareEncoding": { + "type": "boolean", + "description": "Gets or sets a value indicating whether hardware encoding is enabled." + }, + "AllowHevcEncoding": { + "type": "boolean", + "description": "Gets or sets a value indicating whether HEVC encoding is enabled." + }, + "AllowAv1Encoding": { + "type": "boolean", + "description": "Gets or sets a value indicating whether AV1 encoding is enabled." + }, + "EnableSubtitleExtraction": { + "type": "boolean", + "description": "Gets or sets a value indicating whether subtitle extraction is enabled." + }, + "HardwareDecodingCodecs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the codecs hardware encoding is used for.", + "nullable": true + }, + "AllowOnDemandMetadataBasedKeyframeExtractionForExtensions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class EncodingOptions." + }, + "EndPointInfo": { + "type": "object", + "properties": { + "IsLocal": { + "type": "boolean" + }, + "IsInNetwork": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ExternalIdInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the display name of the external id provider (IE: IMDB, MusicBrainz, etc)." + }, + "Key": { + "type": "string", + "description": "Gets or sets the unique key for this id. This key should be unique across all providers." + }, + "Type": { + "enum": [ + "Album", + "AlbumArtist", + "Artist", + "BoxSet", + "Episode", + "Movie", + "OtherArtist", + "Person", + "ReleaseGroup", + "Season", + "Series", + "Track", + "Book", + "Recording" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ExternalIdMediaType" + } + ], + "description": "Gets or sets the specific media type for this id. This is used to distinguish between the different\r\nexternal id types for providers with multiple ids.\r\nA null value indicates there is no specific media type associated with the external id, or this is the\r\ndefault id for the external provider so there is no need to specify a type.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Represents the external id information for serialization to the client." + }, + "ExternalIdMediaType": { + "enum": [ + "Album", + "AlbumArtist", + "Artist", + "BoxSet", + "Episode", + "Movie", + "OtherArtist", + "Person", + "ReleaseGroup", + "Season", + "Series", + "Track", + "Book", + "Recording" + ], + "type": "string", + "description": "The specific media type of an MediaBrowser.Model.Providers.ExternalIdInfo." + }, + "ExternalUrl": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "Url": { + "type": "string", + "description": "Gets or sets the type of the item.", + "nullable": true + } + }, + "additionalProperties": false + }, + "ExtraType": { + "enum": [ + "Unknown", + "Clip", + "Trailer", + "BehindTheScenes", + "DeletedScene", + "Interview", + "Scene", + "Sample", + "ThemeSong", + "ThemeVideo", + "Featurette", + "Short" + ], + "type": "string" + }, + "FileSystemEntryInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets the name." + }, + "Path": { + "type": "string", + "description": "Gets the path." + }, + "Type": { + "enum": [ + "File", + "Directory", + "NetworkComputer", + "NetworkShare" + ], + "allOf": [ + { + "$ref": "#/components/schemas/FileSystemEntryType" + } + ], + "description": "Gets the type." + } + }, + "additionalProperties": false, + "description": "Class FileSystemEntryInfo." + }, + "FileSystemEntryType": { + "enum": [ + "File", + "Directory", + "NetworkComputer", + "NetworkShare" + ], + "type": "string", + "description": "Enum FileSystemEntryType." + }, + "FolderStorageDto": { + "type": "object", + "properties": { + "Path": { + "type": "string", + "description": "Gets the path of the folder in question." + }, + "FreeSpace": { + "type": "integer", + "description": "Gets the free space of the underlying storage device of the Jellyfin.Api.Models.SystemInfoDtos.FolderStorageDto.Path.", + "format": "int64" + }, + "UsedSpace": { + "type": "integer", + "description": "Gets the used space of the underlying storage device of the Jellyfin.Api.Models.SystemInfoDtos.FolderStorageDto.Path.", + "format": "int64" + }, + "StorageType": { + "type": "string", + "description": "Gets the kind of storage device of the Jellyfin.Api.Models.SystemInfoDtos.FolderStorageDto.Path.", + "nullable": true + }, + "DeviceId": { + "type": "string", + "description": "Gets the Device Identifier.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Contains information about a specific folder." + }, + "FontFile": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "Size": { + "type": "integer", + "description": "Gets or sets the size.", + "format": "int64" + }, + "DateCreated": { + "type": "string", + "description": "Gets or sets the date created.", + "format": "date-time" + }, + "DateModified": { + "type": "string", + "description": "Gets or sets the date modified.", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "Class FontFile." + }, + "ForceKeepAliveMessage": { + "type": "object", + "properties": { + "Data": { + "type": "integer", + "description": "Gets or sets the data.", + "format": "int32" + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "ForceKeepAlive", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Force keep alive websocket messages." + }, + "ForgotPasswordAction": { + "enum": [ + "ContactAdmin", + "PinCode", + "InNetworkRequired" + ], + "type": "string" + }, + "ForgotPasswordDto": { + "required": [ + "EnteredUsername" + ], + "type": "object", + "properties": { + "EnteredUsername": { + "type": "string", + "description": "Gets or sets the entered username to have its password reset." + } + }, + "additionalProperties": false, + "description": "Forgot Password request body DTO." + }, + "ForgotPasswordPinDto": { + "required": [ + "Pin" + ], + "type": "object", + "properties": { + "Pin": { + "type": "string", + "description": "Gets or sets the entered pin to have the password reset." + } + }, + "additionalProperties": false, + "description": "Forgot Password Pin enter request body DTO." + }, + "ForgotPasswordResult": { + "type": "object", + "properties": { + "Action": { + "enum": [ + "ContactAdmin", + "PinCode", + "InNetworkRequired" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ForgotPasswordAction" + } + ], + "description": "Gets or sets the action." + }, + "PinFile": { + "type": "string", + "description": "Gets or sets the pin file.", + "nullable": true + }, + "PinExpirationDate": { + "type": "string", + "description": "Gets or sets the pin expiration date.", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false + }, + "GeneralCommand": { + "type": "object", + "properties": { + "Name": { + "enum": [ + "MoveUp", + "MoveDown", + "MoveLeft", + "MoveRight", + "PageUp", + "PageDown", + "PreviousLetter", + "NextLetter", + "ToggleOsd", + "ToggleContextMenu", + "Select", + "Back", + "TakeScreenshot", + "SendKey", + "SendString", + "GoHome", + "GoToSettings", + "VolumeUp", + "VolumeDown", + "Mute", + "Unmute", + "ToggleMute", + "SetVolume", + "SetAudioStreamIndex", + "SetSubtitleStreamIndex", + "ToggleFullscreen", + "DisplayContent", + "GoToSearch", + "DisplayMessage", + "SetRepeatMode", + "ChannelUp", + "ChannelDown", + "Guide", + "ToggleStats", + "PlayMediaSource", + "PlayTrailers", + "SetShuffleQueue", + "PlayState", + "PlayNext", + "ToggleOsdMenu", + "Play", + "SetMaxStreamingBitrate", + "SetPlaybackOrder" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GeneralCommandType" + } + ], + "description": "This exists simply to identify a set of known commands." + }, + "ControllingUserId": { + "type": "string", + "format": "uuid" + }, + "Arguments": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + } + } + }, + "additionalProperties": false + }, + "GeneralCommandMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/GeneralCommand" + } + ], + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "GeneralCommand", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "General command websocket message." + }, + "GeneralCommandType": { + "enum": [ + "MoveUp", + "MoveDown", + "MoveLeft", + "MoveRight", + "PageUp", + "PageDown", + "PreviousLetter", + "NextLetter", + "ToggleOsd", + "ToggleContextMenu", + "Select", + "Back", + "TakeScreenshot", + "SendKey", + "SendString", + "GoHome", + "GoToSettings", + "VolumeUp", + "VolumeDown", + "Mute", + "Unmute", + "ToggleMute", + "SetVolume", + "SetAudioStreamIndex", + "SetSubtitleStreamIndex", + "ToggleFullscreen", + "DisplayContent", + "GoToSearch", + "DisplayMessage", + "SetRepeatMode", + "ChannelUp", + "ChannelDown", + "Guide", + "ToggleStats", + "PlayMediaSource", + "PlayTrailers", + "SetShuffleQueue", + "PlayState", + "PlayNext", + "ToggleOsdMenu", + "Play", + "SetMaxStreamingBitrate", + "SetPlaybackOrder" + ], + "type": "string", + "description": "This exists simply to identify a set of known commands." + }, + "GetProgramsDto": { + "type": "object", + "properties": { + "ChannelIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Gets or sets the channels to return guide information for.", + "nullable": true + }, + "UserId": { + "type": "string", + "description": "Gets or sets optional. Filter by user id.", + "format": "uuid", + "nullable": true + }, + "MinStartDate": { + "type": "string", + "description": "Gets or sets the minimum premiere start date.", + "format": "date-time", + "nullable": true + }, + "HasAired": { + "type": "boolean", + "description": "Gets or sets filter by programs that have completed airing, or not.", + "nullable": true + }, + "IsAiring": { + "type": "boolean", + "description": "Gets or sets filter by programs that are currently airing, or not.", + "nullable": true + }, + "MaxStartDate": { + "type": "string", + "description": "Gets or sets the maximum premiere start date.", + "format": "date-time", + "nullable": true + }, + "MinEndDate": { + "type": "string", + "description": "Gets or sets the minimum premiere end date.", + "format": "date-time", + "nullable": true + }, + "MaxEndDate": { + "type": "string", + "description": "Gets or sets the maximum premiere end date.", + "format": "date-time", + "nullable": true + }, + "IsMovie": { + "type": "boolean", + "description": "Gets or sets filter for movies.", + "nullable": true + }, + "IsSeries": { + "type": "boolean", + "description": "Gets or sets filter for series.", + "nullable": true + }, + "IsNews": { + "type": "boolean", + "description": "Gets or sets filter for news.", + "nullable": true + }, + "IsKids": { + "type": "boolean", + "description": "Gets or sets filter for kids.", + "nullable": true + }, + "IsSports": { + "type": "boolean", + "description": "Gets or sets filter for sports.", + "nullable": true + }, + "StartIndex": { + "type": "integer", + "description": "Gets or sets the record index to start at. All items with a lower index will be dropped from the results.", + "format": "int32", + "nullable": true + }, + "Limit": { + "type": "integer", + "description": "Gets or sets the maximum number of records to return.", + "format": "int32", + "nullable": true + }, + "SortBy": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemSortBy" + }, + "description": "Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.", + "nullable": true + }, + "SortOrder": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortOrder" + }, + "description": "Gets or sets sort order.", + "nullable": true + }, + "Genres": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the genres to return guide information for.", + "nullable": true + }, + "GenreIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Gets or sets the genre ids to return guide information for.", + "nullable": true + }, + "EnableImages": { + "type": "boolean", + "description": "Gets or sets include image information in output.", + "nullable": true + }, + "EnableTotalRecordCount": { + "type": "boolean", + "description": "Gets or sets a value indicating whether retrieve total record count.", + "default": true + }, + "ImageTypeLimit": { + "type": "integer", + "description": "Gets or sets the max number of images to return, per image type.", + "format": "int32", + "nullable": true + }, + "EnableImageTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + }, + "description": "Gets or sets the image types to include in the output.", + "nullable": true + }, + "EnableUserData": { + "type": "boolean", + "description": "Gets or sets include user data.", + "nullable": true + }, + "SeriesTimerId": { + "type": "string", + "description": "Gets or sets filter by series timer id.", + "nullable": true + }, + "LibrarySeriesId": { + "type": "string", + "description": "Gets or sets filter by library series id.", + "format": "uuid", + "nullable": true + }, + "Fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemFields" + }, + "description": "Gets or sets specify additional fields of information to return in the output.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Get programs dto." + }, + "GroupInfoDto": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid" + }, + "GroupName": { + "type": "string", + "description": "Gets the group name." + }, + "State": { + "enum": [ + "Idle", + "Waiting", + "Paused", + "Playing" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupStateType" + } + ], + "description": "Gets the group state." + }, + "Participants": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets the participants." + }, + "LastUpdatedAt": { + "type": "string", + "description": "Gets the date when this DTO has been created.", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "Class GroupInfoDto." + }, + "GroupQueueMode": { + "enum": [ + "Queue", + "QueueNext" + ], + "type": "string", + "description": "Enum GroupQueueMode." + }, + "GroupRepeatMode": { + "enum": [ + "RepeatOne", + "RepeatAll", + "RepeatNone" + ], + "type": "string", + "description": "Enum GroupRepeatMode." + }, + "GroupShuffleMode": { + "enum": [ + "Sorted", + "Shuffle" + ], + "type": "string", + "description": "Enum GroupShuffleMode." + }, + "GroupStateType": { + "enum": [ + "Idle", + "Waiting", + "Paused", + "Playing" + ], + "type": "string", + "description": "Enum GroupState." + }, + "GroupStateUpdate": { + "type": "object", + "properties": { + "State": { + "enum": [ + "Idle", + "Waiting", + "Paused", + "Playing" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupStateType" + } + ], + "description": "Gets the state of the group." + }, + "Reason": { + "enum": [ + "Play", + "SetPlaylistItem", + "RemoveFromPlaylist", + "MovePlaylistItem", + "Queue", + "Unpause", + "Pause", + "Stop", + "Seek", + "Buffer", + "Ready", + "NextItem", + "PreviousItem", + "SetRepeatMode", + "SetShuffleMode", + "Ping", + "IgnoreWait" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackRequestType" + } + ], + "description": "Gets the reason of the state change." + } + }, + "additionalProperties": false, + "description": "Class GroupStateUpdate." + }, + "GroupUpdate": { + "type": "object", + "oneOf": [ + { + "$ref": "#/components/schemas/SyncPlayGroupDoesNotExistUpdate" + }, + { + "$ref": "#/components/schemas/SyncPlayGroupJoinedUpdate" + }, + { + "$ref": "#/components/schemas/SyncPlayGroupLeftUpdate" + }, + { + "$ref": "#/components/schemas/SyncPlayLibraryAccessDeniedUpdate" + }, + { + "$ref": "#/components/schemas/SyncPlayNotInGroupUpdate" + }, + { + "$ref": "#/components/schemas/SyncPlayPlayQueueUpdate" + }, + { + "$ref": "#/components/schemas/SyncPlayStateUpdate" + }, + { + "$ref": "#/components/schemas/SyncPlayUserJoinedUpdate" + }, + { + "$ref": "#/components/schemas/SyncPlayUserLeftUpdate" + } + ], + "description": "Represents the list of possible group update types", + "discriminator": { + "propertyName": "Type", + "mapping": { + "GroupDoesNotExist": "#/components/schemas/SyncPlayGroupDoesNotExistUpdate", + "GroupJoined": "#/components/schemas/SyncPlayGroupJoinedUpdate", + "GroupLeft": "#/components/schemas/SyncPlayGroupLeftUpdate", + "LibraryAccessDenied": "#/components/schemas/SyncPlayLibraryAccessDeniedUpdate", + "NotInGroup": "#/components/schemas/SyncPlayNotInGroupUpdate", + "PlayQueue": "#/components/schemas/SyncPlayPlayQueueUpdate", + "StateUpdate": "#/components/schemas/SyncPlayStateUpdate", + "UserJoined": "#/components/schemas/SyncPlayUserJoinedUpdate", + "UserLeft": "#/components/schemas/SyncPlayUserLeftUpdate" + } + } + }, + "GroupUpdateType": { + "enum": [ + "UserJoined", + "UserLeft", + "GroupJoined", + "GroupLeft", + "StateUpdate", + "PlayQueue", + "NotInGroup", + "GroupDoesNotExist", + "LibraryAccessDenied" + ], + "type": "string", + "description": "Enum GroupUpdateType." + }, + "GuideInfo": { + "type": "object", + "properties": { + "StartDate": { + "type": "string", + "description": "Gets or sets the start date.", + "format": "date-time" + }, + "EndDate": { + "type": "string", + "description": "Gets or sets the end date.", + "format": "date-time" + } + }, + "additionalProperties": false + }, + "HardwareAccelerationType": { + "enum": [ + "none", + "amf", + "qsv", + "nvenc", + "v4l2m2m", + "vaapi", + "videotoolbox", + "rkmpp" + ], + "type": "string", + "description": "Enum containing hardware acceleration types." + }, + "IgnoreWaitRequestDto": { + "type": "object", + "properties": { + "IgnoreWait": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the client should be ignored." + } + }, + "additionalProperties": false, + "description": "Class IgnoreWaitRequestDto." + }, + "ImageFormat": { + "enum": [ + "Bmp", + "Gif", + "Jpg", + "Png", + "Webp", + "Svg" + ], + "type": "string", + "description": "Enum ImageOutputFormat." + }, + "ImageInfo": { + "type": "object", + "properties": { + "ImageType": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Gets or sets the type of the image." + }, + "ImageIndex": { + "type": "integer", + "description": "Gets or sets the index of the image.", + "format": "int32", + "nullable": true + }, + "ImageTag": { + "type": "string", + "description": "Gets or sets the image tag.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "BlurHash": { + "type": "string", + "description": "Gets or sets the blurhash.", + "nullable": true + }, + "Height": { + "type": "integer", + "description": "Gets or sets the height.", + "format": "int32", + "nullable": true + }, + "Width": { + "type": "integer", + "description": "Gets or sets the width.", + "format": "int32", + "nullable": true + }, + "Size": { + "type": "integer", + "description": "Gets or sets the size.", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "Class ImageInfo." + }, + "ImageOption": { + "type": "object", + "properties": { + "Type": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Gets or sets the type." + }, + "Limit": { + "type": "integer", + "description": "Gets or sets the limit.", + "format": "int32" + }, + "MinWidth": { + "type": "integer", + "description": "Gets or sets the minimum width.", + "format": "int32" + } + }, + "additionalProperties": false + }, + "ImageOrientation": { + "enum": [ + "TopLeft", + "TopRight", + "BottomRight", + "BottomLeft", + "LeftTop", + "RightTop", + "RightBottom", + "LeftBottom" + ], + "type": "string" + }, + "ImageProviderInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets the name." + }, + "SupportedImages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + }, + "description": "Gets the supported image types." + } + }, + "additionalProperties": false, + "description": "Class ImageProviderInfo." + }, + "ImageResolution": { + "enum": [ + "MatchSource", + "P144", + "P240", + "P360", + "P480", + "P720", + "P1080", + "P1440", + "P2160" + ], + "type": "string", + "description": "Enum ImageResolution." + }, + "ImageSavingConvention": { + "enum": [ + "Legacy", + "Compatible" + ], + "type": "string" + }, + "ImageType": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "type": "string", + "description": "Enum ImageType." + }, + "InboundKeepAliveMessage": { + "type": "object", + "properties": { + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "KeepAlive", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Keep alive websocket messages." + }, + "InboundWebSocketMessage": { + "type": "object", + "oneOf": [ + { + "$ref": "#/components/schemas/ActivityLogEntryStartMessage" + }, + { + "$ref": "#/components/schemas/ActivityLogEntryStopMessage" + }, + { + "$ref": "#/components/schemas/InboundKeepAliveMessage" + }, + { + "$ref": "#/components/schemas/ScheduledTasksInfoStartMessage" + }, + { + "$ref": "#/components/schemas/ScheduledTasksInfoStopMessage" + }, + { + "$ref": "#/components/schemas/SessionsStartMessage" + }, + { + "$ref": "#/components/schemas/SessionsStopMessage" + } + ], + "description": "Represents the list of possible inbound websocket types", + "discriminator": { + "propertyName": "MessageType", + "mapping": { + "ActivityLogEntryStart": "#/components/schemas/ActivityLogEntryStartMessage", + "ActivityLogEntryStop": "#/components/schemas/ActivityLogEntryStopMessage", + "KeepAlive": "#/components/schemas/InboundKeepAliveMessage", + "ScheduledTasksInfoStart": "#/components/schemas/ScheduledTasksInfoStartMessage", + "ScheduledTasksInfoStop": "#/components/schemas/ScheduledTasksInfoStopMessage", + "SessionsStart": "#/components/schemas/SessionsStartMessage", + "SessionsStop": "#/components/schemas/SessionsStopMessage" + } + } + }, + "InstallationInfo": { + "type": "object", + "properties": { + "Guid": { + "type": "string", + "description": "Gets or sets the Id.", + "format": "uuid" + }, + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "Version": { + "type": "string", + "description": "Gets or sets the version.", + "nullable": true + }, + "Changelog": { + "type": "string", + "description": "Gets or sets the changelog for this version.", + "nullable": true + }, + "SourceUrl": { + "type": "string", + "description": "Gets or sets the source URL.", + "nullable": true + }, + "Checksum": { + "type": "string", + "description": "Gets or sets a checksum for the binary.", + "nullable": true + }, + "PackageInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/PackageInfo" + } + ], + "description": "Gets or sets package information for the installation.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class InstallationInfo." + }, + "IPlugin": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets the name of the plugin.", + "nullable": true, + "readOnly": true + }, + "Description": { + "type": "string", + "description": "Gets the Description.", + "nullable": true, + "readOnly": true + }, + "Id": { + "type": "string", + "description": "Gets the unique id.", + "format": "uuid", + "readOnly": true + }, + "Version": { + "type": "string", + "description": "Gets the plugin version.", + "nullable": true, + "readOnly": true + }, + "AssemblyFilePath": { + "type": "string", + "description": "Gets the path to the assembly file.", + "nullable": true, + "readOnly": true + }, + "CanUninstall": { + "type": "boolean", + "description": "Gets a value indicating whether the plugin can be uninstalled.", + "readOnly": true + }, + "DataFolderPath": { + "type": "string", + "description": "Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Defines the MediaBrowser.Common.Plugins.IPlugin." + }, + "IsoType": { + "enum": [ + "Dvd", + "BluRay" + ], + "type": "string", + "description": "Enum IsoType." + }, + "ItemCounts": { + "type": "object", + "properties": { + "MovieCount": { + "type": "integer", + "description": "Gets or sets the movie count.", + "format": "int32" + }, + "SeriesCount": { + "type": "integer", + "description": "Gets or sets the series count.", + "format": "int32" + }, + "EpisodeCount": { + "type": "integer", + "description": "Gets or sets the episode count.", + "format": "int32" + }, + "ArtistCount": { + "type": "integer", + "description": "Gets or sets the artist count.", + "format": "int32" + }, + "ProgramCount": { + "type": "integer", + "description": "Gets or sets the program count.", + "format": "int32" + }, + "TrailerCount": { + "type": "integer", + "description": "Gets or sets the trailer count.", + "format": "int32" + }, + "SongCount": { + "type": "integer", + "description": "Gets or sets the song count.", + "format": "int32" + }, + "AlbumCount": { + "type": "integer", + "description": "Gets or sets the album count.", + "format": "int32" + }, + "MusicVideoCount": { + "type": "integer", + "description": "Gets or sets the music video count.", + "format": "int32" + }, + "BoxSetCount": { + "type": "integer", + "description": "Gets or sets the box set count.", + "format": "int32" + }, + "BookCount": { + "type": "integer", + "description": "Gets or sets the book count.", + "format": "int32" + }, + "ItemCount": { + "type": "integer", + "description": "Gets or sets the item count.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Class LibrarySummary." + }, + "ItemFields": { + "enum": [ + "AirTime", + "CanDelete", + "CanDownload", + "ChannelInfo", + "Chapters", + "Trickplay", + "ChildCount", + "CumulativeRunTimeTicks", + "CustomRating", + "DateCreated", + "DateLastMediaAdded", + "DisplayPreferencesId", + "Etag", + "ExternalUrls", + "Genres", + "ItemCounts", + "MediaSourceCount", + "MediaSources", + "OriginalTitle", + "Overview", + "ParentId", + "Path", + "People", + "PlayAccess", + "ProductionLocations", + "ProviderIds", + "PrimaryImageAspectRatio", + "RecursiveItemCount", + "Settings", + "SeriesStudio", + "SortName", + "SpecialEpisodeNumbers", + "Studios", + "Taglines", + "Tags", + "RemoteTrailers", + "MediaStreams", + "SeasonUserData", + "DateLastRefreshed", + "DateLastSaved", + "RefreshState", + "ChannelImage", + "EnableMediaSourceDisplay", + "Width", + "Height", + "ExtraIds", + "LocalTrailerCount", + "IsHD", + "SpecialFeatureCount" + ], + "type": "string", + "description": "Used to control the data that gets attached to DtoBaseItems." + }, + "ItemFilter": { + "enum": [ + "IsFolder", + "IsNotFolder", + "IsUnplayed", + "IsPlayed", + "IsFavorite", + "IsResumable", + "Likes", + "Dislikes", + "IsFavoriteOrLikes" + ], + "type": "string", + "description": "Enum ItemFilter." + }, + "ItemSortBy": { + "enum": [ + "Default", + "AiredEpisodeOrder", + "Album", + "AlbumArtist", + "Artist", + "DateCreated", + "OfficialRating", + "DatePlayed", + "PremiereDate", + "StartDate", + "SortName", + "Name", + "Random", + "Runtime", + "CommunityRating", + "ProductionYear", + "PlayCount", + "CriticRating", + "IsFolder", + "IsUnplayed", + "IsPlayed", + "SeriesSortName", + "VideoBitRate", + "AirTime", + "Studio", + "IsFavoriteOrLiked", + "DateLastContentAdded", + "SeriesDatePlayed", + "ParentIndexNumber", + "IndexNumber" + ], + "type": "string", + "description": "These represent sort orders." + }, + "JoinGroupRequestDto": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets or sets the group identifier.", + "format": "uuid" + } + }, + "additionalProperties": false, + "description": "Class JoinGroupRequestDto." + }, + "KeepUntil": { + "enum": [ + "UntilDeleted", + "UntilSpaceNeeded", + "UntilWatched", + "UntilDate" + ], + "type": "string" + }, + "LibraryChangedMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/LibraryUpdateInfo" + } + ], + "description": "Class LibraryUpdateInfo.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "LibraryChanged", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Library changed message." + }, + "LibraryOptionInfoDto": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets name.", + "nullable": true + }, + "DefaultEnabled": { + "type": "boolean", + "description": "Gets or sets a value indicating whether default enabled." + } + }, + "additionalProperties": false, + "description": "Library option info dto." + }, + "LibraryOptions": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean" + }, + "EnablePhotos": { + "type": "boolean" + }, + "EnableRealtimeMonitor": { + "type": "boolean" + }, + "EnableLUFSScan": { + "type": "boolean" + }, + "EnableChapterImageExtraction": { + "type": "boolean" + }, + "ExtractChapterImagesDuringLibraryScan": { + "type": "boolean" + }, + "EnableTrickplayImageExtraction": { + "type": "boolean" + }, + "ExtractTrickplayImagesDuringLibraryScan": { + "type": "boolean" + }, + "PathInfos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaPathInfo" + } + }, + "SaveLocalMetadata": { + "type": "boolean" + }, + "EnableInternetProviders": { + "type": "boolean", + "deprecated": true + }, + "EnableAutomaticSeriesGrouping": { + "type": "boolean" + }, + "EnableEmbeddedTitles": { + "type": "boolean" + }, + "EnableEmbeddedExtrasTitles": { + "type": "boolean" + }, + "EnableEmbeddedEpisodeInfos": { + "type": "boolean" + }, + "AutomaticRefreshIntervalDays": { + "type": "integer", + "format": "int32" + }, + "PreferredMetadataLanguage": { + "type": "string", + "description": "Gets or sets the preferred metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "SeasonZeroDisplayName": { + "type": "string" + }, + "MetadataSavers": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "DisabledLocalMetadataReaders": { + "type": "array", + "items": { + "type": "string" + } + }, + "LocalMetadataReaderOrder": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "DisabledSubtitleFetchers": { + "type": "array", + "items": { + "type": "string" + } + }, + "SubtitleFetcherOrder": { + "type": "array", + "items": { + "type": "string" + } + }, + "DisabledMediaSegmentProviders": { + "type": "array", + "items": { + "type": "string" + } + }, + "MediaSegmentProviderOrder": { + "type": "array", + "items": { + "type": "string" + } + }, + "SkipSubtitlesIfEmbeddedSubtitlesPresent": { + "type": "boolean" + }, + "SkipSubtitlesIfAudioTrackMatches": { + "type": "boolean" + }, + "SubtitleDownloadLanguages": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "RequirePerfectSubtitleMatch": { + "type": "boolean" + }, + "SaveSubtitlesWithMedia": { + "type": "boolean" + }, + "SaveLyricsWithMedia": { + "type": "boolean", + "default": false + }, + "SaveTrickplayWithMedia": { + "type": "boolean", + "default": false + }, + "DisabledLyricFetchers": { + "type": "array", + "items": { + "type": "string" + } + }, + "LyricFetcherOrder": { + "type": "array", + "items": { + "type": "string" + } + }, + "PreferNonstandardArtistsTag": { + "type": "boolean", + "default": false + }, + "UseCustomTagDelimiters": { + "type": "boolean", + "default": false + }, + "CustomTagDelimiters": { + "type": "array", + "items": { + "type": "string" + } + }, + "DelimiterWhitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "AutomaticallyAddToCollection": { + "type": "boolean" + }, + "AllowEmbeddedSubtitles": { + "enum": [ + "AllowAll", + "AllowText", + "AllowImage", + "AllowNone" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EmbeddedSubtitleOptions" + } + ], + "description": "An enum representing the options to disable embedded subs." + }, + "TypeOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TypeOptions" + } + } + }, + "additionalProperties": false + }, + "LibraryOptionsResultDto": { + "type": "object", + "properties": { + "MetadataSavers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryOptionInfoDto" + }, + "description": "Gets or sets the metadata savers." + }, + "MetadataReaders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryOptionInfoDto" + }, + "description": "Gets or sets the metadata readers." + }, + "SubtitleFetchers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryOptionInfoDto" + }, + "description": "Gets or sets the subtitle fetchers." + }, + "LyricFetchers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryOptionInfoDto" + }, + "description": "Gets or sets the list of lyric fetchers." + }, + "MediaSegmentProviders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryOptionInfoDto" + }, + "description": "Gets or sets the list of MediaSegment Providers." + }, + "TypeOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryTypeOptionsDto" + }, + "description": "Gets or sets the type options." + } + }, + "additionalProperties": false, + "description": "Library options result dto." + }, + "LibraryStorageDto": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "description": "Gets or sets the Library Id.", + "format": "uuid" + }, + "Name": { + "type": "string", + "description": "Gets or sets the name of the library." + }, + "Folders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderStorageDto" + }, + "description": "Gets or sets the storage informations about the folders used in a library." + } + }, + "additionalProperties": false, + "description": "Contains informations about a libraries storage informations." + }, + "LibraryTypeOptionsDto": { + "type": "object", + "properties": { + "Type": { + "type": "string", + "description": "Gets or sets the type.", + "nullable": true + }, + "MetadataFetchers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryOptionInfoDto" + }, + "description": "Gets or sets the metadata fetchers." + }, + "ImageFetchers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryOptionInfoDto" + }, + "description": "Gets or sets the image fetchers." + }, + "SupportedImageTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageType" + }, + "description": "Gets or sets the supported image types." + }, + "DefaultImageOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageOption" + }, + "description": "Gets or sets the default image options." + } + }, + "additionalProperties": false, + "description": "Library type options dto." + }, + "LibraryUpdateInfo": { + "type": "object", + "properties": { + "FoldersAddedTo": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the folders added to." + }, + "FoldersRemovedFrom": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the folders removed from." + }, + "ItemsAdded": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the items added." + }, + "ItemsRemoved": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the items removed." + }, + "ItemsUpdated": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the items updated." + }, + "CollectionFolders": { + "type": "array", + "items": { + "type": "string" + } + }, + "IsEmpty": { + "type": "boolean", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Class LibraryUpdateInfo." + }, + "ListingsProviderInfo": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "nullable": true + }, + "Type": { + "type": "string", + "nullable": true + }, + "Username": { + "type": "string", + "nullable": true + }, + "Password": { + "type": "string", + "nullable": true + }, + "ListingsId": { + "type": "string", + "nullable": true + }, + "ZipCode": { + "type": "string", + "nullable": true + }, + "Country": { + "type": "string", + "nullable": true + }, + "Path": { + "type": "string", + "nullable": true + }, + "EnabledTuners": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "EnableAllTuners": { + "type": "boolean" + }, + "NewsCategories": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "SportsCategories": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "KidsCategories": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "MovieCategories": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "ChannelMappings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameValuePair" + }, + "nullable": true + }, + "MoviePrefix": { + "type": "string", + "nullable": true + }, + "PreferredLanguage": { + "type": "string", + "nullable": true + }, + "UserAgent": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LiveStreamResponse": { + "type": "object", + "properties": { + "MediaSource": { + "allOf": [ + { + "$ref": "#/components/schemas/MediaSourceInfo" + } + ] + } + }, + "additionalProperties": false + }, + "LiveTvInfo": { + "type": "object", + "properties": { + "Services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LiveTvServiceInfo" + }, + "description": "Gets or sets the services." + }, + "IsEnabled": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is enabled." + }, + "EnabledUsers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the enabled users." + } + }, + "additionalProperties": false + }, + "LiveTvOptions": { + "type": "object", + "properties": { + "GuideDays": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "RecordingPath": { + "type": "string", + "nullable": true + }, + "MovieRecordingPath": { + "type": "string", + "nullable": true + }, + "SeriesRecordingPath": { + "type": "string", + "nullable": true + }, + "EnableRecordingSubfolders": { + "type": "boolean" + }, + "EnableOriginalAudioWithEncodedRecordings": { + "type": "boolean" + }, + "TunerHosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TunerHostInfo" + }, + "nullable": true + }, + "ListingProviders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ListingsProviderInfo" + }, + "nullable": true + }, + "PrePaddingSeconds": { + "type": "integer", + "format": "int32" + }, + "PostPaddingSeconds": { + "type": "integer", + "format": "int32" + }, + "MediaLocationsCreated": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "RecordingPostProcessor": { + "type": "string", + "nullable": true + }, + "RecordingPostProcessorArguments": { + "type": "string", + "nullable": true + }, + "SaveRecordingNFO": { + "type": "boolean" + }, + "SaveRecordingImages": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "LiveTvServiceInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "HomePageUrl": { + "type": "string", + "description": "Gets or sets the home page URL.", + "nullable": true + }, + "Status": { + "enum": [ + "Ok", + "Unavailable" + ], + "allOf": [ + { + "$ref": "#/components/schemas/LiveTvServiceStatus" + } + ], + "description": "Gets or sets the status." + }, + "StatusMessage": { + "type": "string", + "description": "Gets or sets the status message.", + "nullable": true + }, + "Version": { + "type": "string", + "description": "Gets or sets the version.", + "nullable": true + }, + "HasUpdateAvailable": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance has update available." + }, + "IsVisible": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is visible." + }, + "Tuners": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class ServiceInfo." + }, + "LiveTvServiceStatus": { + "enum": [ + "Ok", + "Unavailable" + ], + "type": "string" + }, + "LocalizationOption": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "nullable": true + }, + "Value": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LocationType": { + "enum": [ + "FileSystem", + "Remote", + "Virtual", + "Offline" + ], + "type": "string", + "description": "Enum LocationType." + }, + "LogFile": { + "type": "object", + "properties": { + "DateCreated": { + "type": "string", + "description": "Gets or sets the date created.", + "format": "date-time" + }, + "DateModified": { + "type": "string", + "description": "Gets or sets the date modified.", + "format": "date-time" + }, + "Size": { + "type": "integer", + "description": "Gets or sets the size.", + "format": "int64" + }, + "Name": { + "type": "string", + "description": "Gets or sets the name." + } + }, + "additionalProperties": false + }, + "LogLevel": { + "enum": [ + "Trace", + "Debug", + "Information", + "Warning", + "Error", + "Critical", + "None" + ], + "type": "string" + }, + "LyricDto": { + "type": "object", + "properties": { + "Metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/LyricMetadata" + } + ], + "description": "Gets or sets Metadata for the lyrics." + }, + "Lyrics": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LyricLine" + }, + "description": "Gets or sets a collection of individual lyric lines." + } + }, + "additionalProperties": false, + "description": "LyricResponse model." + }, + "LyricLine": { + "type": "object", + "properties": { + "Text": { + "type": "string", + "description": "Gets the text of this lyric line." + }, + "Start": { + "type": "integer", + "description": "Gets the start time in ticks.", + "format": "int64", + "nullable": true + }, + "Cues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LyricLineCue" + }, + "description": "Gets the time-aligned cues for the song's lyrics.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Lyric model." + }, + "LyricLineCue": { + "type": "object", + "properties": { + "Position": { + "type": "integer", + "description": "Gets the start character index of the cue.", + "format": "int32" + }, + "EndPosition": { + "type": "integer", + "description": "Gets the end character index of the cue.", + "format": "int32" + }, + "Start": { + "type": "integer", + "description": "Gets the timestamp the lyric is synced to in ticks.", + "format": "int64" + }, + "End": { + "type": "integer", + "description": "Gets the end timestamp the lyric is synced to in ticks.", + "format": "int64", + "nullable": true + } + }, + "additionalProperties": false, + "description": "LyricLineCue model, holds information about the timing of words within a LyricLine." + }, + "LyricMetadata": { + "type": "object", + "properties": { + "Artist": { + "type": "string", + "description": "Gets or sets the song artist.", + "nullable": true + }, + "Album": { + "type": "string", + "description": "Gets or sets the album this song is on.", + "nullable": true + }, + "Title": { + "type": "string", + "description": "Gets or sets the title of the song.", + "nullable": true + }, + "Author": { + "type": "string", + "description": "Gets or sets the author of the lyric data.", + "nullable": true + }, + "Length": { + "type": "integer", + "description": "Gets or sets the length of the song in ticks.", + "format": "int64", + "nullable": true + }, + "By": { + "type": "string", + "description": "Gets or sets who the LRC file was created by.", + "nullable": true + }, + "Offset": { + "type": "integer", + "description": "Gets or sets the lyric offset compared to audio in ticks.", + "format": "int64", + "nullable": true + }, + "Creator": { + "type": "string", + "description": "Gets or sets the software used to create the LRC file.", + "nullable": true + }, + "Version": { + "type": "string", + "description": "Gets or sets the version of the creator used.", + "nullable": true + }, + "IsSynced": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this lyric is synced.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "LyricMetadata model." + }, + "MediaAttachment": { + "type": "object", + "properties": { + "Codec": { + "type": "string", + "description": "Gets or sets the codec.", + "nullable": true + }, + "CodecTag": { + "type": "string", + "description": "Gets or sets the codec tag.", + "nullable": true + }, + "Comment": { + "type": "string", + "description": "Gets or sets the comment.", + "nullable": true + }, + "Index": { + "type": "integer", + "description": "Gets or sets the index.", + "format": "int32" + }, + "FileName": { + "type": "string", + "description": "Gets or sets the filename.", + "nullable": true + }, + "MimeType": { + "type": "string", + "description": "Gets or sets the MIME type.", + "nullable": true + }, + "DeliveryUrl": { + "type": "string", + "description": "Gets or sets the delivery URL.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class MediaAttachment." + }, + "MediaPathDto": { + "required": [ + "Name" + ], + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name of the library." + }, + "Path": { + "type": "string", + "description": "Gets or sets the path to add.", + "nullable": true + }, + "PathInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/MediaPathInfo" + } + ], + "description": "Gets or sets the path info.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Media Path dto." + }, + "MediaPathInfo": { + "type": "object", + "properties": { + "Path": { + "type": "string" + } + }, + "additionalProperties": false + }, + "MediaProtocol": { + "enum": [ + "File", + "Http", + "Rtmp", + "Rtsp", + "Udp", + "Rtp", + "Ftp" + ], + "type": "string" + }, + "MediaSegmentDto": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "description": "Gets or sets the id of the media segment.", + "format": "uuid" + }, + "ItemId": { + "type": "string", + "description": "Gets or sets the id of the associated item.", + "format": "uuid" + }, + "Type": { + "enum": [ + "Unknown", + "Commercial", + "Preview", + "Recap", + "Outro", + "Intro" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaSegmentType" + } + ], + "description": "Gets or sets the type of content this segment defines.", + "default": "Unknown" + }, + "StartTicks": { + "type": "integer", + "description": "Gets or sets the start of the segment.", + "format": "int64" + }, + "EndTicks": { + "type": "integer", + "description": "Gets or sets the end of the segment.", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "Api model for MediaSegment's." + }, + "MediaSegmentDtoQueryResult": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaSegmentDto" + }, + "description": "Gets or sets the items." + }, + "TotalRecordCount": { + "type": "integer", + "description": "Gets or sets the total number of records available.", + "format": "int32" + }, + "StartIndex": { + "type": "integer", + "description": "Gets or sets the index of the first record in Items.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Query result container." + }, + "MediaSegmentType": { + "enum": [ + "Unknown", + "Commercial", + "Preview", + "Recap", + "Outro", + "Intro" + ], + "type": "string", + "description": "Defines the types of content an individual Jellyfin.Database.Implementations.Entities.MediaSegment represents." + }, + "MediaSourceInfo": { + "type": "object", + "properties": { + "Protocol": { + "enum": [ + "File", + "Http", + "Rtmp", + "Rtsp", + "Udp", + "Rtp", + "Ftp" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaProtocol" + } + ] + }, + "Id": { + "type": "string", + "nullable": true + }, + "Path": { + "type": "string", + "nullable": true + }, + "EncoderPath": { + "type": "string", + "nullable": true + }, + "EncoderProtocol": { + "enum": [ + "File", + "Http", + "Rtmp", + "Rtsp", + "Udp", + "Rtp", + "Ftp" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaProtocol" + } + ], + "nullable": true + }, + "Type": { + "enum": [ + "Default", + "Grouping", + "Placeholder" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaSourceType" + } + ] + }, + "Container": { + "type": "string", + "nullable": true + }, + "Size": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "Name": { + "type": "string", + "nullable": true + }, + "IsRemote": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the media is remote.\r\nDifferentiate internet url vs local network." + }, + "ETag": { + "type": "string", + "nullable": true + }, + "RunTimeTicks": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "ReadAtNativeFramerate": { + "type": "boolean" + }, + "IgnoreDts": { + "type": "boolean" + }, + "IgnoreIndex": { + "type": "boolean" + }, + "GenPtsInput": { + "type": "boolean" + }, + "SupportsTranscoding": { + "type": "boolean" + }, + "SupportsDirectStream": { + "type": "boolean" + }, + "SupportsDirectPlay": { + "type": "boolean" + }, + "IsInfiniteStream": { + "type": "boolean" + }, + "UseMostCompatibleTranscodingProfile": { + "type": "boolean", + "default": false + }, + "RequiresOpening": { + "type": "boolean" + }, + "OpenToken": { + "type": "string", + "nullable": true + }, + "RequiresClosing": { + "type": "boolean" + }, + "LiveStreamId": { + "type": "string", + "nullable": true + }, + "BufferMs": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "RequiresLooping": { + "type": "boolean" + }, + "SupportsProbing": { + "type": "boolean" + }, + "VideoType": { + "enum": [ + "VideoFile", + "Iso", + "Dvd", + "BluRay" + ], + "allOf": [ + { + "$ref": "#/components/schemas/VideoType" + } + ], + "nullable": true + }, + "IsoType": { + "enum": [ + "Dvd", + "BluRay" + ], + "allOf": [ + { + "$ref": "#/components/schemas/IsoType" + } + ], + "nullable": true + }, + "Video3DFormat": { + "enum": [ + "HalfSideBySide", + "FullSideBySide", + "FullTopAndBottom", + "HalfTopAndBottom", + "MVC" + ], + "allOf": [ + { + "$ref": "#/components/schemas/Video3DFormat" + } + ], + "nullable": true + }, + "MediaStreams": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaStream" + }, + "nullable": true + }, + "MediaAttachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaAttachment" + }, + "nullable": true + }, + "Formats": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "Bitrate": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "FallbackMaxStreamingBitrate": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "Timestamp": { + "enum": [ + "None", + "Zero", + "Valid" + ], + "allOf": [ + { + "$ref": "#/components/schemas/TransportStreamTimestamp" + } + ], + "nullable": true + }, + "RequiredHttpHeaders": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "nullable": true + }, + "TranscodingUrl": { + "type": "string", + "nullable": true + }, + "TranscodingSubProtocol": { + "enum": [ + "http", + "hls" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaStreamProtocol" + } + ], + "description": "Media streaming protocol.\r\nLowercase for backwards compatibility." + }, + "TranscodingContainer": { + "type": "string", + "nullable": true + }, + "AnalyzeDurationMs": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "DefaultAudioStreamIndex": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "DefaultSubtitleStreamIndex": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "HasSegments": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "MediaSourceType": { + "enum": [ + "Default", + "Grouping", + "Placeholder" + ], + "type": "string" + }, + "MediaStream": { + "type": "object", + "properties": { + "Codec": { + "type": "string", + "description": "Gets or sets the codec.", + "nullable": true + }, + "CodecTag": { + "type": "string", + "description": "Gets or sets the codec tag.", + "nullable": true + }, + "Language": { + "type": "string", + "description": "Gets or sets the language.", + "nullable": true + }, + "ColorRange": { + "type": "string", + "description": "Gets or sets the color range.", + "nullable": true + }, + "ColorSpace": { + "type": "string", + "description": "Gets or sets the color space.", + "nullable": true + }, + "ColorTransfer": { + "type": "string", + "description": "Gets or sets the color transfer.", + "nullable": true + }, + "ColorPrimaries": { + "type": "string", + "description": "Gets or sets the color primaries.", + "nullable": true + }, + "DvVersionMajor": { + "type": "integer", + "description": "Gets or sets the Dolby Vision version major.", + "format": "int32", + "nullable": true + }, + "DvVersionMinor": { + "type": "integer", + "description": "Gets or sets the Dolby Vision version minor.", + "format": "int32", + "nullable": true + }, + "DvProfile": { + "type": "integer", + "description": "Gets or sets the Dolby Vision profile.", + "format": "int32", + "nullable": true + }, + "DvLevel": { + "type": "integer", + "description": "Gets or sets the Dolby Vision level.", + "format": "int32", + "nullable": true + }, + "RpuPresentFlag": { + "type": "integer", + "description": "Gets or sets the Dolby Vision rpu present flag.", + "format": "int32", + "nullable": true + }, + "ElPresentFlag": { + "type": "integer", + "description": "Gets or sets the Dolby Vision el present flag.", + "format": "int32", + "nullable": true + }, + "BlPresentFlag": { + "type": "integer", + "description": "Gets or sets the Dolby Vision bl present flag.", + "format": "int32", + "nullable": true + }, + "DvBlSignalCompatibilityId": { + "type": "integer", + "description": "Gets or sets the Dolby Vision bl signal compatibility id.", + "format": "int32", + "nullable": true + }, + "Rotation": { + "type": "integer", + "description": "Gets or sets the Rotation in degrees.", + "format": "int32", + "nullable": true + }, + "Comment": { + "type": "string", + "description": "Gets or sets the comment.", + "nullable": true + }, + "TimeBase": { + "type": "string", + "description": "Gets or sets the time base.", + "nullable": true + }, + "CodecTimeBase": { + "type": "string", + "description": "Gets or sets the codec time base.", + "nullable": true + }, + "Title": { + "type": "string", + "description": "Gets or sets the title.", + "nullable": true + }, + "Hdr10PlusPresentFlag": { + "type": "boolean", + "nullable": true + }, + "VideoRange": { + "enum": [ + "Unknown", + "SDR", + "HDR" + ], + "allOf": [ + { + "$ref": "#/components/schemas/VideoRange" + } + ], + "description": "Gets the video range.", + "default": "Unknown", + "readOnly": true + }, + "VideoRangeType": { + "enum": [ + "Unknown", + "SDR", + "HDR10", + "HLG", + "DOVI", + "DOVIWithHDR10", + "DOVIWithHLG", + "DOVIWithSDR", + "DOVIWithEL", + "DOVIWithHDR10Plus", + "DOVIWithELHDR10Plus", + "DOVIInvalid", + "HDR10Plus" + ], + "allOf": [ + { + "$ref": "#/components/schemas/VideoRangeType" + } + ], + "description": "Gets the video range type.", + "default": "Unknown", + "readOnly": true + }, + "VideoDoViTitle": { + "type": "string", + "description": "Gets the video dovi title.", + "nullable": true, + "readOnly": true + }, + "AudioSpatialFormat": { + "enum": [ + "None", + "DolbyAtmos", + "DTSX" + ], + "allOf": [ + { + "$ref": "#/components/schemas/AudioSpatialFormat" + } + ], + "description": "Gets the audio spatial format.", + "default": "None", + "readOnly": true + }, + "LocalizedUndefined": { + "type": "string", + "nullable": true + }, + "LocalizedDefault": { + "type": "string", + "nullable": true + }, + "LocalizedForced": { + "type": "string", + "nullable": true + }, + "LocalizedExternal": { + "type": "string", + "nullable": true + }, + "LocalizedHearingImpaired": { + "type": "string", + "nullable": true + }, + "DisplayTitle": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "NalLengthSize": { + "type": "string", + "nullable": true + }, + "IsInterlaced": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is interlaced." + }, + "IsAVC": { + "type": "boolean", + "nullable": true + }, + "ChannelLayout": { + "type": "string", + "description": "Gets or sets the channel layout.", + "nullable": true + }, + "BitRate": { + "type": "integer", + "description": "Gets or sets the bit rate.", + "format": "int32", + "nullable": true + }, + "BitDepth": { + "type": "integer", + "description": "Gets or sets the bit depth.", + "format": "int32", + "nullable": true + }, + "RefFrames": { + "type": "integer", + "description": "Gets or sets the reference frames.", + "format": "int32", + "nullable": true + }, + "PacketLength": { + "type": "integer", + "description": "Gets or sets the length of the packet.", + "format": "int32", + "nullable": true + }, + "Channels": { + "type": "integer", + "description": "Gets or sets the channels.", + "format": "int32", + "nullable": true + }, + "SampleRate": { + "type": "integer", + "description": "Gets or sets the sample rate.", + "format": "int32", + "nullable": true + }, + "IsDefault": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is default." + }, + "IsForced": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is forced." + }, + "IsHearingImpaired": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is for the hearing impaired." + }, + "Height": { + "type": "integer", + "description": "Gets or sets the height.", + "format": "int32", + "nullable": true + }, + "Width": { + "type": "integer", + "description": "Gets or sets the width.", + "format": "int32", + "nullable": true + }, + "AverageFrameRate": { + "type": "number", + "description": "Gets or sets the average frame rate.", + "format": "float", + "nullable": true + }, + "RealFrameRate": { + "type": "number", + "description": "Gets or sets the real frame rate.", + "format": "float", + "nullable": true + }, + "ReferenceFrameRate": { + "type": "number", + "description": "Gets the framerate used as reference.\r\nPrefer AverageFrameRate, if that is null or an unrealistic value\r\nthen fallback to RealFrameRate.", + "format": "float", + "nullable": true, + "readOnly": true + }, + "Profile": { + "type": "string", + "description": "Gets or sets the profile.", + "nullable": true + }, + "Type": { + "enum": [ + "Audio", + "Video", + "Subtitle", + "EmbeddedImage", + "Data", + "Lyric" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaStreamType" + } + ], + "description": "Gets or sets the type." + }, + "AspectRatio": { + "type": "string", + "description": "Gets or sets the aspect ratio.", + "nullable": true + }, + "Index": { + "type": "integer", + "description": "Gets or sets the index.", + "format": "int32" + }, + "Score": { + "type": "integer", + "description": "Gets or sets the score.", + "format": "int32", + "nullable": true + }, + "IsExternal": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is external." + }, + "DeliveryMethod": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ], + "description": "Gets or sets the method.", + "nullable": true + }, + "DeliveryUrl": { + "type": "string", + "description": "Gets or sets the delivery URL.", + "nullable": true + }, + "IsExternalUrl": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is external URL.", + "nullable": true + }, + "IsTextSubtitleStream": { + "type": "boolean", + "readOnly": true + }, + "SupportsExternalStream": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [supports external stream]." + }, + "Path": { + "type": "string", + "description": "Gets or sets the filename.", + "nullable": true + }, + "PixelFormat": { + "type": "string", + "description": "Gets or sets the pixel format.", + "nullable": true + }, + "Level": { + "type": "number", + "description": "Gets or sets the level.", + "format": "double", + "nullable": true + }, + "IsAnamorphic": { + "type": "boolean", + "description": "Gets or sets whether this instance is anamorphic.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class MediaStream." + }, + "MediaStreamProtocol": { + "enum": [ + "http", + "hls" + ], + "type": "string", + "description": "Media streaming protocol.\r\nLowercase for backwards compatibility." + }, + "MediaStreamType": { + "enum": [ + "Audio", + "Video", + "Subtitle", + "EmbeddedImage", + "Data", + "Lyric" + ], + "type": "string", + "description": "Enum MediaStreamType." + }, + "MediaType": { + "enum": [ + "Unknown", + "Video", + "Audio", + "Photo", + "Book" + ], + "type": "string", + "description": "Media types." + }, + "MediaUpdateInfoDto": { + "type": "object", + "properties": { + "Updates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaUpdateInfoPathDto" + }, + "description": "Gets or sets the list of updates." + } + }, + "additionalProperties": false, + "description": "Media Update Info Dto." + }, + "MediaUpdateInfoPathDto": { + "type": "object", + "properties": { + "Path": { + "type": "string", + "description": "Gets or sets media path.", + "nullable": true + }, + "UpdateType": { + "type": "string", + "description": "Gets or sets media update type.\r\nCreated, Modified, Deleted.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "The media update info path." + }, + "MediaUrl": { + "type": "object", + "properties": { + "Url": { + "type": "string", + "nullable": true + }, + "Name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "MessageCommand": { + "required": [ + "Text" + ], + "type": "object", + "properties": { + "Header": { + "type": "string", + "nullable": true + }, + "Text": { + "type": "string" + }, + "TimeoutMs": { + "type": "integer", + "format": "int64", + "nullable": true + } + }, + "additionalProperties": false + }, + "MetadataConfiguration": { + "type": "object", + "properties": { + "UseFileCreationTimeForDateAdded": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "MetadataEditorInfo": { + "type": "object", + "properties": { + "ParentalRatingOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ParentalRating" + }, + "description": "Gets or sets the parental rating options." + }, + "Countries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountryInfo" + }, + "description": "Gets or sets the countries." + }, + "Cultures": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CultureDto" + }, + "description": "Gets or sets the cultures." + }, + "ExternalIdInfos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalIdInfo" + }, + "description": "Gets or sets the external id infos." + }, + "ContentType": { + "enum": [ + "unknown", + "movies", + "tvshows", + "music", + "musicvideos", + "trailers", + "homevideos", + "boxsets", + "books", + "photos", + "livetv", + "playlists", + "folders" + ], + "allOf": [ + { + "$ref": "#/components/schemas/CollectionType" + } + ], + "description": "Gets or sets the content type.", + "nullable": true + }, + "ContentTypeOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameValuePair" + }, + "description": "Gets or sets the content type options." + } + }, + "additionalProperties": false, + "description": "A class representing metadata editor information." + }, + "MetadataField": { + "enum": [ + "Cast", + "Genres", + "ProductionLocations", + "Studios", + "Tags", + "Name", + "Overview", + "Runtime", + "OfficialRating" + ], + "type": "string", + "description": "Enum MetadataFields." + }, + "MetadataOptions": { + "type": "object", + "properties": { + "ItemType": { + "type": "string", + "nullable": true + }, + "DisabledMetadataSavers": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "LocalMetadataReaderOrder": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "DisabledMetadataFetchers": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "MetadataFetcherOrder": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "DisabledImageFetchers": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "ImageFetcherOrder": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class MetadataOptions." + }, + "MetadataRefreshMode": { + "enum": [ + "None", + "ValidationOnly", + "Default", + "FullRefresh" + ], + "type": "string" + }, + "MovePlaylistItemRequestDto": { + "type": "object", + "properties": { + "PlaylistItemId": { + "type": "string", + "description": "Gets or sets the playlist identifier of the item.", + "format": "uuid" + }, + "NewIndex": { + "type": "integer", + "description": "Gets or sets the new position.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Class MovePlaylistItemRequestDto." + }, + "MovieInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "description": "Gets or sets the original title.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "MetadataLanguage": { + "type": "string", + "description": "Gets or sets the metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "Year": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "IsAutomated": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "MovieInfoRemoteSearchQuery": { + "type": "object", + "properties": { + "SearchInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/MovieInfo" + } + ], + "nullable": true + }, + "ItemId": { + "type": "string", + "format": "uuid" + }, + "SearchProviderName": { + "type": "string", + "description": "Gets or sets the provider name to search within if set.", + "nullable": true + }, + "IncludeDisabledProviders": { + "type": "boolean", + "description": "Gets or sets a value indicating whether disabled providers should be included." + } + }, + "additionalProperties": false + }, + "MusicVideoInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "description": "Gets or sets the original title.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "MetadataLanguage": { + "type": "string", + "description": "Gets or sets the metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "Year": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "IsAutomated": { + "type": "boolean" + }, + "Artists": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "MusicVideoInfoRemoteSearchQuery": { + "type": "object", + "properties": { + "SearchInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/MusicVideoInfo" + } + ], + "nullable": true + }, + "ItemId": { + "type": "string", + "format": "uuid" + }, + "SearchProviderName": { + "type": "string", + "description": "Gets or sets the provider name to search within if set.", + "nullable": true + }, + "IncludeDisabledProviders": { + "type": "boolean", + "description": "Gets or sets a value indicating whether disabled providers should be included." + } + }, + "additionalProperties": false + }, + "NameGuidPair": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "nullable": true + }, + "Id": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, + "NameIdPair": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the identifier.", + "nullable": true + } + }, + "additionalProperties": false + }, + "NameValuePair": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "Value": { + "type": "string", + "description": "Gets or sets the value.", + "nullable": true + } + }, + "additionalProperties": false + }, + "NetworkConfiguration": { + "type": "object", + "properties": { + "BaseUrl": { + "type": "string", + "description": "Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at." + }, + "EnableHttps": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to use HTTPS." + }, + "RequireHttps": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the server should force connections over HTTPS." + }, + "CertificatePath": { + "type": "string", + "description": "Gets or sets the filesystem path of an X.509 certificate to use for SSL." + }, + "CertificatePassword": { + "type": "string", + "description": "Gets or sets the password required to access the X.509 certificate data in the file specified by MediaBrowser.Common.Net.NetworkConfiguration.CertificatePath." + }, + "InternalHttpPort": { + "type": "integer", + "description": "Gets or sets the internal HTTP server port.", + "format": "int32" + }, + "InternalHttpsPort": { + "type": "integer", + "description": "Gets or sets the internal HTTPS server port.", + "format": "int32" + }, + "PublicHttpPort": { + "type": "integer", + "description": "Gets or sets the public HTTP port.", + "format": "int32" + }, + "PublicHttpsPort": { + "type": "integer", + "description": "Gets or sets the public HTTPS port.", + "format": "int32" + }, + "AutoDiscovery": { + "type": "boolean", + "description": "Gets or sets a value indicating whether Autodiscovery is enabled." + }, + "EnableUPnP": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to enable automatic port forwarding.", + "deprecated": true + }, + "EnableIPv4": { + "type": "boolean", + "description": "Gets or sets a value indicating whether IPv6 is enabled." + }, + "EnableIPv6": { + "type": "boolean", + "description": "Gets or sets a value indicating whether IPv6 is enabled." + }, + "EnableRemoteAccess": { + "type": "boolean", + "description": "Gets or sets a value indicating whether access from outside of the LAN is permitted." + }, + "LocalNetworkSubnets": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the subnets that are deemed to make up the LAN." + }, + "LocalNetworkAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used." + }, + "KnownProxies": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the known proxies." + }, + "IgnoreVirtualInterfaces": { + "type": "boolean", + "description": "Gets or sets a value indicating whether address names that match MediaBrowser.Common.Net.NetworkConfiguration.VirtualInterfaceNames should be ignored for the purposes of binding." + }, + "VirtualInterfaceNames": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. ." + }, + "EnablePublishedServerUriByRequest": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the published server uri is based on information in HTTP requests." + }, + "PublishedServerUriBySubnet": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the PublishedServerUriBySubnet\r\nGets or sets PublishedServerUri to advertise for specific subnets." + }, + "RemoteIPFilter": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the filter for remote IP connectivity. Used in conjunction with ." + }, + "IsRemoteIPFilterBlacklist": { + "type": "boolean", + "description": "Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist." + } + }, + "additionalProperties": false, + "description": "Defines the MediaBrowser.Common.Net.NetworkConfiguration." + }, + "NewGroupRequestDto": { + "type": "object", + "properties": { + "GroupName": { + "type": "string", + "description": "Gets or sets the group name." + } + }, + "additionalProperties": false, + "description": "Class NewGroupRequestDto." + }, + "NextItemRequestDto": { + "type": "object", + "properties": { + "PlaylistItemId": { + "type": "string", + "description": "Gets or sets the playing item identifier.", + "format": "uuid" + } + }, + "additionalProperties": false, + "description": "Class NextItemRequestDto." + }, + "OpenLiveStreamDto": { + "type": "object", + "properties": { + "OpenToken": { + "type": "string", + "description": "Gets or sets the open token.", + "nullable": true + }, + "UserId": { + "type": "string", + "description": "Gets or sets the user id.", + "format": "uuid", + "nullable": true + }, + "PlaySessionId": { + "type": "string", + "description": "Gets or sets the play session id.", + "nullable": true + }, + "MaxStreamingBitrate": { + "type": "integer", + "description": "Gets or sets the max streaming bitrate.", + "format": "int32", + "nullable": true + }, + "StartTimeTicks": { + "type": "integer", + "description": "Gets or sets the start time in ticks.", + "format": "int64", + "nullable": true + }, + "AudioStreamIndex": { + "type": "integer", + "description": "Gets or sets the audio stream index.", + "format": "int32", + "nullable": true + }, + "SubtitleStreamIndex": { + "type": "integer", + "description": "Gets or sets the subtitle stream index.", + "format": "int32", + "nullable": true + }, + "MaxAudioChannels": { + "type": "integer", + "description": "Gets or sets the max audio channels.", + "format": "int32", + "nullable": true + }, + "ItemId": { + "type": "string", + "description": "Gets or sets the item id.", + "format": "uuid", + "nullable": true + }, + "EnableDirectPlay": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to enable direct play.", + "nullable": true + }, + "EnableDirectStream": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to enable direct stream.", + "nullable": true + }, + "AlwaysBurnInSubtitleWhenTranscoding": { + "type": "boolean", + "description": "Gets or sets a value indicating whether always burn in subtitles when transcoding.", + "nullable": true + }, + "DeviceProfile": { + "allOf": [ + { + "$ref": "#/components/schemas/DeviceProfile" + } + ], + "description": "A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.\r\n
\r\nSpecifically, it defines the supported containers and\r\ncodecs (video and/or audio, including codec profiles and levels)\r\nthe device is able to direct play (without transcoding or remuxing),\r\nas well as which containers/codecs to transcode to in case it isn't.", + "nullable": true + }, + "DirectPlayProtocols": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaProtocol" + }, + "description": "Gets or sets the device play protocols." + } + }, + "additionalProperties": false, + "description": "Open live stream dto." + }, + "OutboundKeepAliveMessage": { + "type": "object", + "properties": { + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "KeepAlive", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Keep alive websocket messages." + }, + "OutboundWebSocketMessage": { + "type": "object", + "oneOf": [ + { + "$ref": "#/components/schemas/ActivityLogEntryMessage" + }, + { + "$ref": "#/components/schemas/ForceKeepAliveMessage" + }, + { + "$ref": "#/components/schemas/GeneralCommandMessage" + }, + { + "$ref": "#/components/schemas/LibraryChangedMessage" + }, + { + "$ref": "#/components/schemas/OutboundKeepAliveMessage" + }, + { + "$ref": "#/components/schemas/PlayMessage" + }, + { + "$ref": "#/components/schemas/PlaystateMessage" + }, + { + "$ref": "#/components/schemas/PluginInstallationCancelledMessage" + }, + { + "$ref": "#/components/schemas/PluginInstallationCompletedMessage" + }, + { + "$ref": "#/components/schemas/PluginInstallationFailedMessage" + }, + { + "$ref": "#/components/schemas/PluginInstallingMessage" + }, + { + "$ref": "#/components/schemas/PluginUninstalledMessage" + }, + { + "$ref": "#/components/schemas/RefreshProgressMessage" + }, + { + "$ref": "#/components/schemas/RestartRequiredMessage" + }, + { + "$ref": "#/components/schemas/ScheduledTaskEndedMessage" + }, + { + "$ref": "#/components/schemas/ScheduledTasksInfoMessage" + }, + { + "$ref": "#/components/schemas/SeriesTimerCancelledMessage" + }, + { + "$ref": "#/components/schemas/SeriesTimerCreatedMessage" + }, + { + "$ref": "#/components/schemas/ServerRestartingMessage" + }, + { + "$ref": "#/components/schemas/ServerShuttingDownMessage" + }, + { + "$ref": "#/components/schemas/SessionsMessage" + }, + { + "$ref": "#/components/schemas/SyncPlayCommandMessage" + }, + { + "$ref": "#/components/schemas/TimerCancelledMessage" + }, + { + "$ref": "#/components/schemas/TimerCreatedMessage" + }, + { + "$ref": "#/components/schemas/UserDataChangedMessage" + }, + { + "$ref": "#/components/schemas/UserDeletedMessage" + }, + { + "$ref": "#/components/schemas/UserUpdatedMessage" + }, + { + "$ref": "#/components/schemas/SyncPlayGroupUpdateMessage" + } + ], + "description": "Represents the list of possible outbound websocket types", + "discriminator": { + "propertyName": "MessageType", + "mapping": { + "ActivityLogEntry": "#/components/schemas/ActivityLogEntryMessage", + "ForceKeepAlive": "#/components/schemas/ForceKeepAliveMessage", + "GeneralCommand": "#/components/schemas/GeneralCommandMessage", + "LibraryChanged": "#/components/schemas/LibraryChangedMessage", + "KeepAlive": "#/components/schemas/OutboundKeepAliveMessage", + "Play": "#/components/schemas/PlayMessage", + "Playstate": "#/components/schemas/PlaystateMessage", + "PackageInstallationCancelled": "#/components/schemas/PluginInstallationCancelledMessage", + "PackageInstallationCompleted": "#/components/schemas/PluginInstallationCompletedMessage", + "PackageInstallationFailed": "#/components/schemas/PluginInstallationFailedMessage", + "PackageInstalling": "#/components/schemas/PluginInstallingMessage", + "PackageUninstalled": "#/components/schemas/PluginUninstalledMessage", + "RefreshProgress": "#/components/schemas/RefreshProgressMessage", + "RestartRequired": "#/components/schemas/RestartRequiredMessage", + "ScheduledTaskEnded": "#/components/schemas/ScheduledTaskEndedMessage", + "ScheduledTasksInfo": "#/components/schemas/ScheduledTasksInfoMessage", + "SeriesTimerCancelled": "#/components/schemas/SeriesTimerCancelledMessage", + "SeriesTimerCreated": "#/components/schemas/SeriesTimerCreatedMessage", + "ServerRestarting": "#/components/schemas/ServerRestartingMessage", + "ServerShuttingDown": "#/components/schemas/ServerShuttingDownMessage", + "Sessions": "#/components/schemas/SessionsMessage", + "SyncPlayCommand": "#/components/schemas/SyncPlayCommandMessage", + "TimerCancelled": "#/components/schemas/TimerCancelledMessage", + "TimerCreated": "#/components/schemas/TimerCreatedMessage", + "UserDataChanged": "#/components/schemas/UserDataChangedMessage", + "UserDeleted": "#/components/schemas/UserDeletedMessage", + "UserUpdated": "#/components/schemas/UserUpdatedMessage", + "SyncPlayGroupUpdate": "#/components/schemas/SyncPlayGroupUpdateMessage" + } + } + }, + "PackageInfo": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Gets or sets the name." + }, + "description": { + "type": "string", + "description": "Gets or sets a long description of the plugin containing features or helpful explanations." + }, + "overview": { + "type": "string", + "description": "Gets or sets a short overview of what the plugin does." + }, + "owner": { + "type": "string", + "description": "Gets or sets the owner." + }, + "category": { + "type": "string", + "description": "Gets or sets the category." + }, + "guid": { + "type": "string", + "description": "Gets or sets the guid of the assembly associated with this plugin.\r\nThis is used to identify the proper item for automatic updates.", + "format": "uuid" + }, + "versions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VersionInfo" + }, + "description": "Gets or sets the versions." + }, + "imageUrl": { + "type": "string", + "description": "Gets or sets the image url for the package.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class PackageInfo." + }, + "ParentalRating": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name." + }, + "Value": { + "type": "integer", + "description": "Gets or sets the value.", + "format": "int32", + "nullable": true + }, + "RatingScore": { + "allOf": [ + { + "$ref": "#/components/schemas/ParentalRatingScore" + } + ], + "description": "Gets or sets the rating score.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class ParentalRating." + }, + "ParentalRatingScore": { + "type": "object", + "properties": { + "score": { + "type": "integer", + "description": "Gets or sets the score.", + "format": "int32" + }, + "subScore": { + "type": "integer", + "description": "Gets or sets the sub score.", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false, + "description": "A class representing an parental rating score." + }, + "PathSubstitution": { + "type": "object", + "properties": { + "From": { + "type": "string", + "description": "Gets or sets the value to substitute." + }, + "To": { + "type": "string", + "description": "Gets or sets the value to substitution with." + } + }, + "additionalProperties": false, + "description": "Defines the MediaBrowser.Model.Configuration.PathSubstitution." + }, + "PersonKind": { + "enum": [ + "Unknown", + "Actor", + "Director", + "Composer", + "Writer", + "GuestStar", + "Producer", + "Conductor", + "Lyricist", + "Arranger", + "Engineer", + "Mixer", + "Remixer", + "Creator", + "Artist", + "AlbumArtist", + "Author", + "Illustrator", + "Penciller", + "Inker", + "Colorist", + "Letterer", + "CoverArtist", + "Editor", + "Translator" + ], + "type": "string", + "description": "The person kind." + }, + "PersonLookupInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "description": "Gets or sets the original title.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "MetadataLanguage": { + "type": "string", + "description": "Gets or sets the metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "Year": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "IsAutomated": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "PersonLookupInfoRemoteSearchQuery": { + "type": "object", + "properties": { + "SearchInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/PersonLookupInfo" + } + ], + "nullable": true + }, + "ItemId": { + "type": "string", + "format": "uuid" + }, + "SearchProviderName": { + "type": "string", + "description": "Gets or sets the provider name to search within if set.", + "nullable": true + }, + "IncludeDisabledProviders": { + "type": "boolean", + "description": "Gets or sets a value indicating whether disabled providers should be included." + } + }, + "additionalProperties": false + }, + "PingRequestDto": { + "type": "object", + "properties": { + "Ping": { + "type": "integer", + "description": "Gets or sets the ping time.", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "Class PingRequestDto." + }, + "PinRedeemResult": { + "type": "object", + "properties": { + "Success": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Users.PinRedeemResult is success." + }, + "UsersReset": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the users reset." + } + }, + "additionalProperties": false + }, + "PlayAccess": { + "enum": [ + "Full", + "None" + ], + "type": "string" + }, + "PlaybackErrorCode": { + "enum": [ + "NotAllowed", + "NoCompatibleStream", + "RateLimitExceeded" + ], + "type": "string" + }, + "PlaybackInfoDto": { + "type": "object", + "properties": { + "UserId": { + "type": "string", + "description": "Gets or sets the playback userId.", + "format": "uuid", + "nullable": true + }, + "MaxStreamingBitrate": { + "type": "integer", + "description": "Gets or sets the max streaming bitrate.", + "format": "int32", + "nullable": true + }, + "StartTimeTicks": { + "type": "integer", + "description": "Gets or sets the start time in ticks.", + "format": "int64", + "nullable": true + }, + "AudioStreamIndex": { + "type": "integer", + "description": "Gets or sets the audio stream index.", + "format": "int32", + "nullable": true + }, + "SubtitleStreamIndex": { + "type": "integer", + "description": "Gets or sets the subtitle stream index.", + "format": "int32", + "nullable": true + }, + "MaxAudioChannels": { + "type": "integer", + "description": "Gets or sets the max audio channels.", + "format": "int32", + "nullable": true + }, + "MediaSourceId": { + "type": "string", + "description": "Gets or sets the media source id.", + "nullable": true + }, + "LiveStreamId": { + "type": "string", + "description": "Gets or sets the live stream id.", + "nullable": true + }, + "DeviceProfile": { + "allOf": [ + { + "$ref": "#/components/schemas/DeviceProfile" + } + ], + "description": "A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.\r\n
\r\nSpecifically, it defines the supported containers and\r\ncodecs (video and/or audio, including codec profiles and levels)\r\nthe device is able to direct play (without transcoding or remuxing),\r\nas well as which containers/codecs to transcode to in case it isn't.", + "nullable": true + }, + "EnableDirectPlay": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to enable direct play.", + "nullable": true + }, + "EnableDirectStream": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to enable direct stream.", + "nullable": true + }, + "EnableTranscoding": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to enable transcoding.", + "nullable": true + }, + "AllowVideoStreamCopy": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to enable video stream copy.", + "nullable": true + }, + "AllowAudioStreamCopy": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to allow audio stream copy.", + "nullable": true + }, + "AutoOpenLiveStream": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to auto open the live stream.", + "nullable": true + }, + "AlwaysBurnInSubtitleWhenTranscoding": { + "type": "boolean", + "description": "Gets or sets a value indicating whether always burn in subtitles when transcoding.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Playback info dto." + }, + "PlaybackInfoResponse": { + "type": "object", + "properties": { + "MediaSources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaSourceInfo" + }, + "description": "Gets or sets the media sources." + }, + "PlaySessionId": { + "type": "string", + "description": "Gets or sets the play session identifier.", + "nullable": true + }, + "ErrorCode": { + "enum": [ + "NotAllowed", + "NoCompatibleStream", + "RateLimitExceeded" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackErrorCode" + } + ], + "description": "Gets or sets the error code.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class PlaybackInfoResponse." + }, + "PlaybackOrder": { + "enum": [ + "Default", + "Shuffle" + ], + "type": "string", + "description": "Enum PlaybackOrder." + }, + "PlaybackProgressInfo": { + "type": "object", + "properties": { + "CanSeek": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance can seek." + }, + "Item": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemDto" + } + ], + "description": "Gets or sets the item.", + "nullable": true + }, + "ItemId": { + "type": "string", + "description": "Gets or sets the item identifier.", + "format": "uuid" + }, + "SessionId": { + "type": "string", + "description": "Gets or sets the session id.", + "nullable": true + }, + "MediaSourceId": { + "type": "string", + "description": "Gets or sets the media version identifier.", + "nullable": true + }, + "AudioStreamIndex": { + "type": "integer", + "description": "Gets or sets the index of the audio stream.", + "format": "int32", + "nullable": true + }, + "SubtitleStreamIndex": { + "type": "integer", + "description": "Gets or sets the index of the subtitle stream.", + "format": "int32", + "nullable": true + }, + "IsPaused": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is paused." + }, + "IsMuted": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is muted." + }, + "PositionTicks": { + "type": "integer", + "description": "Gets or sets the position ticks.", + "format": "int64", + "nullable": true + }, + "PlaybackStartTimeTicks": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "VolumeLevel": { + "type": "integer", + "description": "Gets or sets the volume level.", + "format": "int32", + "nullable": true + }, + "Brightness": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "AspectRatio": { + "type": "string", + "nullable": true + }, + "PlayMethod": { + "enum": [ + "Transcode", + "DirectStream", + "DirectPlay" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlayMethod" + } + ], + "description": "Gets or sets the play method." + }, + "LiveStreamId": { + "type": "string", + "description": "Gets or sets the live stream identifier.", + "nullable": true + }, + "PlaySessionId": { + "type": "string", + "description": "Gets or sets the play session identifier.", + "nullable": true + }, + "RepeatMode": { + "enum": [ + "RepeatNone", + "RepeatAll", + "RepeatOne" + ], + "allOf": [ + { + "$ref": "#/components/schemas/RepeatMode" + } + ], + "description": "Gets or sets the repeat mode." + }, + "PlaybackOrder": { + "enum": [ + "Default", + "Shuffle" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackOrder" + } + ], + "description": "Gets or sets the playback order." + }, + "NowPlayingQueue": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueItem" + }, + "nullable": true + }, + "PlaylistItemId": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class PlaybackProgressInfo." + }, + "PlaybackRequestType": { + "enum": [ + "Play", + "SetPlaylistItem", + "RemoveFromPlaylist", + "MovePlaylistItem", + "Queue", + "Unpause", + "Pause", + "Stop", + "Seek", + "Buffer", + "Ready", + "NextItem", + "PreviousItem", + "SetRepeatMode", + "SetShuffleMode", + "Ping", + "IgnoreWait" + ], + "type": "string", + "description": "Enum PlaybackRequestType." + }, + "PlaybackStartInfo": { + "type": "object", + "properties": { + "CanSeek": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance can seek." + }, + "Item": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemDto" + } + ], + "description": "Gets or sets the item.", + "nullable": true + }, + "ItemId": { + "type": "string", + "description": "Gets or sets the item identifier.", + "format": "uuid" + }, + "SessionId": { + "type": "string", + "description": "Gets or sets the session id.", + "nullable": true + }, + "MediaSourceId": { + "type": "string", + "description": "Gets or sets the media version identifier.", + "nullable": true + }, + "AudioStreamIndex": { + "type": "integer", + "description": "Gets or sets the index of the audio stream.", + "format": "int32", + "nullable": true + }, + "SubtitleStreamIndex": { + "type": "integer", + "description": "Gets or sets the index of the subtitle stream.", + "format": "int32", + "nullable": true + }, + "IsPaused": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is paused." + }, + "IsMuted": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is muted." + }, + "PositionTicks": { + "type": "integer", + "description": "Gets or sets the position ticks.", + "format": "int64", + "nullable": true + }, + "PlaybackStartTimeTicks": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "VolumeLevel": { + "type": "integer", + "description": "Gets or sets the volume level.", + "format": "int32", + "nullable": true + }, + "Brightness": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "AspectRatio": { + "type": "string", + "nullable": true + }, + "PlayMethod": { + "enum": [ + "Transcode", + "DirectStream", + "DirectPlay" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlayMethod" + } + ], + "description": "Gets or sets the play method." + }, + "LiveStreamId": { + "type": "string", + "description": "Gets or sets the live stream identifier.", + "nullable": true + }, + "PlaySessionId": { + "type": "string", + "description": "Gets or sets the play session identifier.", + "nullable": true + }, + "RepeatMode": { + "enum": [ + "RepeatNone", + "RepeatAll", + "RepeatOne" + ], + "allOf": [ + { + "$ref": "#/components/schemas/RepeatMode" + } + ], + "description": "Gets or sets the repeat mode." + }, + "PlaybackOrder": { + "enum": [ + "Default", + "Shuffle" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackOrder" + } + ], + "description": "Gets or sets the playback order." + }, + "NowPlayingQueue": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueItem" + }, + "nullable": true + }, + "PlaylistItemId": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class PlaybackStartInfo." + }, + "PlaybackStopInfo": { + "type": "object", + "properties": { + "Item": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemDto" + } + ], + "description": "Gets or sets the item.", + "nullable": true + }, + "ItemId": { + "type": "string", + "description": "Gets or sets the item identifier.", + "format": "uuid" + }, + "SessionId": { + "type": "string", + "description": "Gets or sets the session id.", + "nullable": true + }, + "MediaSourceId": { + "type": "string", + "description": "Gets or sets the media version identifier.", + "nullable": true + }, + "PositionTicks": { + "type": "integer", + "description": "Gets or sets the position ticks.", + "format": "int64", + "nullable": true + }, + "LiveStreamId": { + "type": "string", + "description": "Gets or sets the live stream identifier.", + "nullable": true + }, + "PlaySessionId": { + "type": "string", + "description": "Gets or sets the play session identifier.", + "nullable": true + }, + "Failed": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Session.PlaybackStopInfo is failed." + }, + "NextMediaType": { + "type": "string", + "nullable": true + }, + "PlaylistItemId": { + "type": "string", + "nullable": true + }, + "NowPlayingQueue": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueItem" + }, + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class PlaybackStopInfo." + }, + "PlayCommand": { + "enum": [ + "PlayNow", + "PlayNext", + "PlayLast", + "PlayInstantMix", + "PlayShuffle" + ], + "type": "string", + "description": "Enum PlayCommand." + }, + "PlayerStateInfo": { + "type": "object", + "properties": { + "PositionTicks": { + "type": "integer", + "description": "Gets or sets the now playing position ticks.", + "format": "int64", + "nullable": true + }, + "CanSeek": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance can seek." + }, + "IsPaused": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is paused." + }, + "IsMuted": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is muted." + }, + "VolumeLevel": { + "type": "integer", + "description": "Gets or sets the volume level.", + "format": "int32", + "nullable": true + }, + "AudioStreamIndex": { + "type": "integer", + "description": "Gets or sets the index of the now playing audio stream.", + "format": "int32", + "nullable": true + }, + "SubtitleStreamIndex": { + "type": "integer", + "description": "Gets or sets the index of the now playing subtitle stream.", + "format": "int32", + "nullable": true + }, + "MediaSourceId": { + "type": "string", + "description": "Gets or sets the now playing media version identifier.", + "nullable": true + }, + "PlayMethod": { + "enum": [ + "Transcode", + "DirectStream", + "DirectPlay" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlayMethod" + } + ], + "description": "Gets or sets the play method.", + "nullable": true + }, + "RepeatMode": { + "enum": [ + "RepeatNone", + "RepeatAll", + "RepeatOne" + ], + "allOf": [ + { + "$ref": "#/components/schemas/RepeatMode" + } + ], + "description": "Gets or sets the repeat mode." + }, + "PlaybackOrder": { + "enum": [ + "Default", + "Shuffle" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlaybackOrder" + } + ], + "description": "Gets or sets the playback order." + }, + "LiveStreamId": { + "type": "string", + "description": "Gets or sets the now playing live stream identifier.", + "nullable": true + } + }, + "additionalProperties": false + }, + "PlaylistCreationResult": { + "type": "object", + "properties": { + "Id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "PlaylistDto": { + "type": "object", + "properties": { + "OpenAccess": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the playlist is publicly readable." + }, + "Shares": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlaylistUserPermissions" + }, + "description": "Gets or sets the share permissions." + }, + "ItemIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Gets or sets the item ids." + } + }, + "additionalProperties": false, + "description": "DTO for playlists." + }, + "PlaylistUserPermissions": { + "type": "object", + "properties": { + "UserId": { + "type": "string", + "description": "Gets or sets the user id.", + "format": "uuid" + }, + "CanEdit": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the user has edit permissions." + } + }, + "additionalProperties": false, + "description": "Class to hold data on user permissions for playlists." + }, + "PlayMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/PlayRequest" + } + ], + "description": "Class PlayRequest.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "Play", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Play command websocket message." + }, + "PlayMethod": { + "enum": [ + "Transcode", + "DirectStream", + "DirectPlay" + ], + "type": "string" + }, + "PlayQueueUpdate": { + "type": "object", + "properties": { + "Reason": { + "enum": [ + "NewPlaylist", + "SetCurrentItem", + "RemoveItems", + "MoveItem", + "Queue", + "QueueNext", + "NextItem", + "PreviousItem", + "RepeatMode", + "ShuffleMode" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlayQueueUpdateReason" + } + ], + "description": "Gets the request type that originated this update." + }, + "LastUpdate": { + "type": "string", + "description": "Gets the UTC time of the last change to the playing queue.", + "format": "date-time" + }, + "Playlist": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SyncPlayQueueItem" + }, + "description": "Gets the playlist." + }, + "PlayingItemIndex": { + "type": "integer", + "description": "Gets the playing item index in the playlist.", + "format": "int32" + }, + "StartPositionTicks": { + "type": "integer", + "description": "Gets the start position ticks.", + "format": "int64" + }, + "IsPlaying": { + "type": "boolean", + "description": "Gets a value indicating whether the current item is playing." + }, + "ShuffleMode": { + "enum": [ + "Sorted", + "Shuffle" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupShuffleMode" + } + ], + "description": "Gets the shuffle mode." + }, + "RepeatMode": { + "enum": [ + "RepeatOne", + "RepeatAll", + "RepeatNone" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupRepeatMode" + } + ], + "description": "Gets the repeat mode." + } + }, + "additionalProperties": false, + "description": "Class PlayQueueUpdate." + }, + "PlayQueueUpdateReason": { + "enum": [ + "NewPlaylist", + "SetCurrentItem", + "RemoveItems", + "MoveItem", + "Queue", + "QueueNext", + "NextItem", + "PreviousItem", + "RepeatMode", + "ShuffleMode" + ], + "type": "string", + "description": "Enum PlayQueueUpdateReason." + }, + "PlayRequest": { + "type": "object", + "properties": { + "ItemIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Gets or sets the item ids.", + "nullable": true + }, + "StartPositionTicks": { + "type": "integer", + "description": "Gets or sets the start position ticks that the first item should be played at.", + "format": "int64", + "nullable": true + }, + "PlayCommand": { + "enum": [ + "PlayNow", + "PlayNext", + "PlayLast", + "PlayInstantMix", + "PlayShuffle" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlayCommand" + } + ], + "description": "Gets or sets the play command." + }, + "ControllingUserId": { + "type": "string", + "description": "Gets or sets the controlling user identifier.", + "format": "uuid" + }, + "SubtitleStreamIndex": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "AudioStreamIndex": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "MediaSourceId": { + "type": "string", + "nullable": true + }, + "StartIndex": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class PlayRequest." + }, + "PlayRequestDto": { + "type": "object", + "properties": { + "PlayingQueue": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Gets or sets the playing queue." + }, + "PlayingItemPosition": { + "type": "integer", + "description": "Gets or sets the position of the playing item in the queue.", + "format": "int32" + }, + "StartPositionTicks": { + "type": "integer", + "description": "Gets or sets the start position ticks.", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "Class PlayRequestDto." + }, + "PlaystateCommand": { + "enum": [ + "Stop", + "Pause", + "Unpause", + "NextTrack", + "PreviousTrack", + "Seek", + "Rewind", + "FastForward", + "PlayPause" + ], + "type": "string", + "description": "Enum PlaystateCommand." + }, + "PlaystateMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/PlaystateRequest" + } + ], + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "Playstate", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Playstate message." + }, + "PlaystateRequest": { + "type": "object", + "properties": { + "Command": { + "enum": [ + "Stop", + "Pause", + "Unpause", + "NextTrack", + "PreviousTrack", + "Seek", + "Rewind", + "FastForward", + "PlayPause" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PlaystateCommand" + } + ], + "description": "Enum PlaystateCommand." + }, + "SeekPositionTicks": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "ControllingUserId": { + "type": "string", + "description": "Gets or sets the controlling user identifier.", + "nullable": true + } + }, + "additionalProperties": false + }, + "PluginInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name." + }, + "Version": { + "type": "string", + "description": "Gets or sets the version." + }, + "ConfigurationFileName": { + "type": "string", + "description": "Gets or sets the name of the configuration file.", + "nullable": true + }, + "Description": { + "type": "string", + "description": "Gets or sets the description." + }, + "Id": { + "type": "string", + "description": "Gets or sets the unique id.", + "format": "uuid" + }, + "CanUninstall": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the plugin can be uninstalled." + }, + "HasImage": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this plugin has a valid image." + }, + "Status": { + "enum": [ + "Active", + "Restart", + "Deleted", + "Superseded", + "Superceded", + "Malfunctioned", + "NotSupported", + "Disabled" + ], + "allOf": [ + { + "$ref": "#/components/schemas/PluginStatus" + } + ], + "description": "Gets or sets a value indicating the status of the plugin." + } + }, + "additionalProperties": false, + "description": "This is a serializable stub class that is used by the api to provide information about installed plugins." + }, + "PluginInstallationCancelledMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/InstallationInfo" + } + ], + "description": "Class InstallationInfo.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "PackageInstallationCancelled", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Plugin installation cancelled message." + }, + "PluginInstallationCompletedMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/InstallationInfo" + } + ], + "description": "Class InstallationInfo.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "PackageInstallationCompleted", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Plugin installation completed message." + }, + "PluginInstallationFailedMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/InstallationInfo" + } + ], + "description": "Class InstallationInfo.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "PackageInstallationFailed", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Plugin installation failed message." + }, + "PluginInstallingMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/InstallationInfo" + } + ], + "description": "Class InstallationInfo.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "PackageInstalling", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Package installing message." + }, + "PluginStatus": { + "enum": [ + "Active", + "Restart", + "Deleted", + "Superseded", + "Superceded", + "Malfunctioned", + "NotSupported", + "Disabled" + ], + "type": "string", + "description": "Plugin load status." + }, + "PluginUninstalledMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginInfo" + } + ], + "description": "This is a serializable stub class that is used by the api to provide information about installed plugins.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "PackageUninstalled", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Plugin uninstalled message." + }, + "PreviousItemRequestDto": { + "type": "object", + "properties": { + "PlaylistItemId": { + "type": "string", + "description": "Gets or sets the playing item identifier.", + "format": "uuid" + } + }, + "additionalProperties": false, + "description": "Class PreviousItemRequestDto." + }, + "ProblemDetails": { + "type": "object", + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "status": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "instance": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": { } + }, + "ProcessPriorityClass": { + "enum": [ + "Normal", + "Idle", + "High", + "RealTime", + "BelowNormal", + "AboveNormal" + ], + "type": "string" + }, + "ProfileCondition": { + "type": "object", + "properties": { + "Condition": { + "enum": [ + "Equals", + "NotEquals", + "LessThanEqual", + "GreaterThanEqual", + "EqualsAny" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ProfileConditionType" + } + ] + }, + "Property": { + "enum": [ + "AudioChannels", + "AudioBitrate", + "AudioProfile", + "Width", + "Height", + "Has64BitOffsets", + "PacketLength", + "VideoBitDepth", + "VideoBitrate", + "VideoFramerate", + "VideoLevel", + "VideoProfile", + "VideoTimestamp", + "IsAnamorphic", + "RefFrames", + "NumAudioStreams", + "NumVideoStreams", + "IsSecondaryAudio", + "VideoCodecTag", + "IsAvc", + "IsInterlaced", + "AudioSampleRate", + "AudioBitDepth", + "VideoRangeType", + "NumStreams" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ProfileConditionValue" + } + ] + }, + "Value": { + "type": "string", + "nullable": true + }, + "IsRequired": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ProfileConditionType": { + "enum": [ + "Equals", + "NotEquals", + "LessThanEqual", + "GreaterThanEqual", + "EqualsAny" + ], + "type": "string" + }, + "ProfileConditionValue": { + "enum": [ + "AudioChannels", + "AudioBitrate", + "AudioProfile", + "Width", + "Height", + "Has64BitOffsets", + "PacketLength", + "VideoBitDepth", + "VideoBitrate", + "VideoFramerate", + "VideoLevel", + "VideoProfile", + "VideoTimestamp", + "IsAnamorphic", + "RefFrames", + "NumAudioStreams", + "NumVideoStreams", + "IsSecondaryAudio", + "VideoCodecTag", + "IsAvc", + "IsInterlaced", + "AudioSampleRate", + "AudioBitDepth", + "VideoRangeType", + "NumStreams" + ], + "type": "string" + }, + "ProgramAudio": { + "enum": [ + "Mono", + "Stereo", + "Dolby", + "DolbyDigital", + "Thx", + "Atmos" + ], + "type": "string" + }, + "PublicSystemInfo": { + "type": "object", + "properties": { + "LocalAddress": { + "type": "string", + "description": "Gets or sets the local address.", + "nullable": true + }, + "ServerName": { + "type": "string", + "description": "Gets or sets the name of the server.", + "nullable": true + }, + "Version": { + "type": "string", + "description": "Gets or sets the server version.", + "nullable": true + }, + "ProductName": { + "type": "string", + "description": "Gets or sets the product name. This is the AssemblyProduct name.", + "nullable": true + }, + "OperatingSystem": { + "type": "string", + "description": "Gets or sets the operating system.", + "nullable": true, + "deprecated": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the id.", + "nullable": true + }, + "StartupWizardCompleted": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the startup wizard is completed.", + "nullable": true + } + }, + "additionalProperties": false + }, + "QueryFilters": { + "type": "object", + "properties": { + "Genres": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameGuidPair" + }, + "nullable": true + }, + "Tags": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "QueryFiltersLegacy": { + "type": "object", + "properties": { + "Genres": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "Tags": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "OfficialRatings": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "Years": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "QueueItem": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "format": "uuid" + }, + "PlaylistItemId": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "QueueRequestDto": { + "type": "object", + "properties": { + "ItemIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Gets or sets the items to enqueue." + }, + "Mode": { + "enum": [ + "Queue", + "QueueNext" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupQueueMode" + } + ], + "description": "Enum GroupQueueMode." + } + }, + "additionalProperties": false, + "description": "Class QueueRequestDto." + }, + "QuickConnectDto": { + "required": [ + "Secret" + ], + "type": "object", + "properties": { + "Secret": { + "type": "string", + "description": "Gets or sets the quick connect secret." + } + }, + "additionalProperties": false, + "description": "The quick connect request body." + }, + "QuickConnectResult": { + "type": "object", + "properties": { + "Authenticated": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this request is authorized." + }, + "Secret": { + "type": "string", + "description": "Gets the secret value used to uniquely identify this request. Can be used to retrieve authentication information." + }, + "Code": { + "type": "string", + "description": "Gets the user facing code used so the user can quickly differentiate this request from others." + }, + "DeviceId": { + "type": "string", + "description": "Gets the requesting device id." + }, + "DeviceName": { + "type": "string", + "description": "Gets the requesting device name." + }, + "AppName": { + "type": "string", + "description": "Gets the requesting app name." + }, + "AppVersion": { + "type": "string", + "description": "Gets the requesting app version." + }, + "DateAdded": { + "type": "string", + "description": "Gets or sets the DateTime that this request was created.", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "Stores the state of an quick connect request." + }, + "RatingType": { + "enum": [ + "Score", + "Likes" + ], + "type": "string" + }, + "ReadyRequestDto": { + "type": "object", + "properties": { + "When": { + "type": "string", + "description": "Gets or sets when the request has been made by the client.", + "format": "date-time" + }, + "PositionTicks": { + "type": "integer", + "description": "Gets or sets the position ticks.", + "format": "int64" + }, + "IsPlaying": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the client playback is unpaused." + }, + "PlaylistItemId": { + "type": "string", + "description": "Gets or sets the playlist item identifier of the playing item.", + "format": "uuid" + } + }, + "additionalProperties": false, + "description": "Class ReadyRequest." + }, + "RecommendationDto": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + }, + "nullable": true + }, + "RecommendationType": { + "enum": [ + "SimilarToRecentlyPlayed", + "SimilarToLikedItem", + "HasDirectorFromRecentlyPlayed", + "HasActorFromRecentlyPlayed", + "HasLikedDirector", + "HasLikedActor" + ], + "allOf": [ + { + "$ref": "#/components/schemas/RecommendationType" + } + ] + }, + "BaselineItemName": { + "type": "string", + "nullable": true + }, + "CategoryId": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, + "RecommendationType": { + "enum": [ + "SimilarToRecentlyPlayed", + "SimilarToLikedItem", + "HasDirectorFromRecentlyPlayed", + "HasActorFromRecentlyPlayed", + "HasLikedDirector", + "HasLikedActor" + ], + "type": "string" + }, + "RecordingStatus": { + "enum": [ + "New", + "InProgress", + "Completed", + "Cancelled", + "ConflictedOk", + "ConflictedNotOk", + "Error" + ], + "type": "string" + }, + "RefreshProgressMessage": { + "type": "object", + "properties": { + "Data": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "RefreshProgress", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Refresh progress message." + }, + "RemoteImageInfo": { + "type": "object", + "properties": { + "ProviderName": { + "type": "string", + "description": "Gets or sets the name of the provider.", + "nullable": true + }, + "Url": { + "type": "string", + "description": "Gets or sets the URL.", + "nullable": true + }, + "ThumbnailUrl": { + "type": "string", + "description": "Gets or sets a url used for previewing a smaller version.", + "nullable": true + }, + "Height": { + "type": "integer", + "description": "Gets or sets the height.", + "format": "int32", + "nullable": true + }, + "Width": { + "type": "integer", + "description": "Gets or sets the width.", + "format": "int32", + "nullable": true + }, + "CommunityRating": { + "type": "number", + "description": "Gets or sets the community rating.", + "format": "double", + "nullable": true + }, + "VoteCount": { + "type": "integer", + "description": "Gets or sets the vote count.", + "format": "int32", + "nullable": true + }, + "Language": { + "type": "string", + "description": "Gets or sets the language.", + "nullable": true + }, + "Type": { + "enum": [ + "Primary", + "Art", + "Backdrop", + "Banner", + "Logo", + "Thumb", + "Disc", + "Box", + "Screenshot", + "Menu", + "Chapter", + "BoxRear", + "Profile" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageType" + } + ], + "description": "Gets or sets the type." + }, + "RatingType": { + "enum": [ + "Score", + "Likes" + ], + "allOf": [ + { + "$ref": "#/components/schemas/RatingType" + } + ], + "description": "Gets or sets the type of the rating." + } + }, + "additionalProperties": false, + "description": "Class RemoteImageInfo." + }, + "RemoteImageResult": { + "type": "object", + "properties": { + "Images": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteImageInfo" + }, + "description": "Gets or sets the images.", + "nullable": true + }, + "TotalRecordCount": { + "type": "integer", + "description": "Gets or sets the total record count.", + "format": "int32" + }, + "Providers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the providers.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class RemoteImageResult." + }, + "RemoteLyricInfoDto": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "description": "Gets or sets the id for the lyric." + }, + "ProviderName": { + "type": "string", + "description": "Gets the provider name." + }, + "Lyrics": { + "allOf": [ + { + "$ref": "#/components/schemas/LyricDto" + } + ], + "description": "Gets the lyrics." + } + }, + "additionalProperties": false, + "description": "The remote lyric info dto." + }, + "RemoteSearchResult": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "ProductionYear": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "IndexNumberEnd": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "ImageUrl": { + "type": "string", + "nullable": true + }, + "SearchProviderName": { + "type": "string", + "nullable": true + }, + "Overview": { + "type": "string", + "nullable": true + }, + "AlbumArtist": { + "allOf": [ + { + "$ref": "#/components/schemas/RemoteSearchResult" + } + ], + "nullable": true + }, + "Artists": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteSearchResult" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "RemoteSubtitleInfo": { + "type": "object", + "properties": { + "ThreeLetterISOLanguageName": { + "type": "string", + "nullable": true + }, + "Id": { + "type": "string", + "nullable": true + }, + "ProviderName": { + "type": "string", + "nullable": true + }, + "Name": { + "type": "string", + "nullable": true + }, + "Format": { + "type": "string", + "nullable": true + }, + "Author": { + "type": "string", + "nullable": true + }, + "Comment": { + "type": "string", + "nullable": true + }, + "DateCreated": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "CommunityRating": { + "type": "number", + "format": "float", + "nullable": true + }, + "FrameRate": { + "type": "number", + "format": "float", + "nullable": true + }, + "DownloadCount": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "IsHashMatch": { + "type": "boolean", + "nullable": true + }, + "AiTranslated": { + "type": "boolean", + "nullable": true + }, + "MachineTranslated": { + "type": "boolean", + "nullable": true + }, + "Forced": { + "type": "boolean", + "nullable": true + }, + "HearingImpaired": { + "type": "boolean", + "nullable": true + } + }, + "additionalProperties": false + }, + "RemoveFromPlaylistRequestDto": { + "type": "object", + "properties": { + "PlaylistItemIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Gets or sets the playlist identifiers of the items. Ignored when clearing the playlist." + }, + "ClearPlaylist": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the entire playlist should be cleared." + }, + "ClearPlayingItem": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the playing item should be removed as well. Used only when clearing the playlist." + } + }, + "additionalProperties": false, + "description": "Class RemoveFromPlaylistRequestDto." + }, + "RepeatMode": { + "enum": [ + "RepeatNone", + "RepeatAll", + "RepeatOne" + ], + "type": "string" + }, + "RepositoryInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "Url": { + "type": "string", + "description": "Gets or sets the URL.", + "nullable": true + }, + "Enabled": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the repository is enabled." + } + }, + "additionalProperties": false, + "description": "Class RepositoryInfo." + }, + "RestartRequiredMessage": { + "type": "object", + "properties": { + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "RestartRequired", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Restart required." + }, + "ScheduledTaskEndedMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/TaskResult" + } + ], + "description": "Class TaskExecutionInfo.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "ScheduledTaskEnded", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Scheduled task ended message." + }, + "ScheduledTasksInfoMessage": { + "type": "object", + "properties": { + "Data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskInfo" + }, + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "ScheduledTasksInfo", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Scheduled tasks info message." + }, + "ScheduledTasksInfoStartMessage": { + "type": "object", + "properties": { + "Data": { + "type": "string", + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "ScheduledTasksInfoStart", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Scheduled tasks info start message.\r\nData is the timing data encoded as \"$initialDelay,$interval\" in ms." + }, + "ScheduledTasksInfoStopMessage": { + "type": "object", + "properties": { + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "ScheduledTasksInfoStop", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Scheduled tasks info stop message." + }, + "ScrollDirection": { + "enum": [ + "Horizontal", + "Vertical" + ], + "type": "string", + "description": "An enum representing the axis that should be scrolled." + }, + "SearchHint": { + "type": "object", + "properties": { + "ItemId": { + "type": "string", + "description": "Gets or sets the item id.", + "format": "uuid", + "deprecated": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the item id.", + "format": "uuid" + }, + "Name": { + "type": "string", + "description": "Gets or sets the name." + }, + "MatchedTerm": { + "type": "string", + "description": "Gets or sets the matched term.", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "description": "Gets or sets the index number.", + "format": "int32", + "nullable": true + }, + "ProductionYear": { + "type": "integer", + "description": "Gets or sets the production year.", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "description": "Gets or sets the parent index number.", + "format": "int32", + "nullable": true + }, + "PrimaryImageTag": { + "type": "string", + "description": "Gets or sets the image tag.", + "nullable": true + }, + "ThumbImageTag": { + "type": "string", + "description": "Gets or sets the thumb image tag.", + "nullable": true + }, + "ThumbImageItemId": { + "type": "string", + "description": "Gets or sets the thumb image item identifier.", + "nullable": true + }, + "BackdropImageTag": { + "type": "string", + "description": "Gets or sets the backdrop image tag.", + "nullable": true + }, + "BackdropImageItemId": { + "type": "string", + "description": "Gets or sets the backdrop image item identifier.", + "nullable": true + }, + "Type": { + "enum": [ + "AggregateFolder", + "Audio", + "AudioBook", + "BasePluginFolder", + "Book", + "BoxSet", + "Channel", + "ChannelFolderItem", + "CollectionFolder", + "Episode", + "Folder", + "Genre", + "ManualPlaylistsFolder", + "Movie", + "LiveTvChannel", + "LiveTvProgram", + "MusicAlbum", + "MusicArtist", + "MusicGenre", + "MusicVideo", + "Person", + "Photo", + "PhotoAlbum", + "Playlist", + "PlaylistsFolder", + "Program", + "Recording", + "Season", + "Series", + "Studio", + "Trailer", + "TvChannel", + "TvProgram", + "UserRootFolder", + "UserView", + "Video", + "Year" + ], + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemKind" + } + ], + "description": "Gets or sets the type." + }, + "IsFolder": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is folder.", + "nullable": true + }, + "RunTimeTicks": { + "type": "integer", + "description": "Gets or sets the run time ticks.", + "format": "int64", + "nullable": true + }, + "MediaType": { + "enum": [ + "Unknown", + "Video", + "Audio", + "Photo", + "Book" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaType" + } + ], + "description": "Gets or sets the type of the media.", + "default": "Unknown" + }, + "StartDate": { + "type": "string", + "description": "Gets or sets the start date.", + "format": "date-time", + "nullable": true + }, + "EndDate": { + "type": "string", + "description": "Gets or sets the end date.", + "format": "date-time", + "nullable": true + }, + "Series": { + "type": "string", + "description": "Gets or sets the series.", + "nullable": true + }, + "Status": { + "type": "string", + "description": "Gets or sets the status.", + "nullable": true + }, + "Album": { + "type": "string", + "description": "Gets or sets the album.", + "nullable": true + }, + "AlbumId": { + "type": "string", + "description": "Gets or sets the album id.", + "format": "uuid", + "nullable": true + }, + "AlbumArtist": { + "type": "string", + "description": "Gets or sets the album artist.", + "nullable": true + }, + "Artists": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the artists." + }, + "SongCount": { + "type": "integer", + "description": "Gets or sets the song count.", + "format": "int32", + "nullable": true + }, + "EpisodeCount": { + "type": "integer", + "description": "Gets or sets the episode count.", + "format": "int32", + "nullable": true + }, + "ChannelId": { + "type": "string", + "description": "Gets or sets the channel identifier.", + "format": "uuid", + "nullable": true + }, + "ChannelName": { + "type": "string", + "description": "Gets or sets the name of the channel.", + "nullable": true + }, + "PrimaryImageAspectRatio": { + "type": "number", + "description": "Gets or sets the primary image aspect ratio.", + "format": "double", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class SearchHintResult." + }, + "SearchHintResult": { + "type": "object", + "properties": { + "SearchHints": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchHint" + }, + "description": "Gets the search hints." + }, + "TotalRecordCount": { + "type": "integer", + "description": "Gets the total record count.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Class SearchHintResult." + }, + "SeekRequestDto": { + "type": "object", + "properties": { + "PositionTicks": { + "type": "integer", + "description": "Gets or sets the position ticks.", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "Class SeekRequestDto." + }, + "SendCommand": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid" + }, + "PlaylistItemId": { + "type": "string", + "description": "Gets the playlist identifier of the playing item.", + "format": "uuid" + }, + "When": { + "type": "string", + "description": "Gets or sets the UTC time when to execute the command.", + "format": "date-time" + }, + "PositionTicks": { + "type": "integer", + "description": "Gets the position ticks.", + "format": "int64", + "nullable": true + }, + "Command": { + "enum": [ + "Unpause", + "Pause", + "Stop", + "Seek" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SendCommandType" + } + ], + "description": "Gets the command." + }, + "EmittedAt": { + "type": "string", + "description": "Gets the UTC time when this command has been emitted.", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "Class SendCommand." + }, + "SendCommandType": { + "enum": [ + "Unpause", + "Pause", + "Stop", + "Seek" + ], + "type": "string", + "description": "Enum SendCommandType." + }, + "SeriesInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "description": "Gets or sets the original title.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "MetadataLanguage": { + "type": "string", + "description": "Gets or sets the metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "Year": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "IsAutomated": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "SeriesInfoRemoteSearchQuery": { + "type": "object", + "properties": { + "SearchInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/SeriesInfo" + } + ], + "nullable": true + }, + "ItemId": { + "type": "string", + "format": "uuid" + }, + "SearchProviderName": { + "type": "string", + "description": "Gets or sets the provider name to search within if set.", + "nullable": true + }, + "IncludeDisabledProviders": { + "type": "boolean", + "description": "Gets or sets a value indicating whether disabled providers should be included." + } + }, + "additionalProperties": false + }, + "SeriesStatus": { + "enum": [ + "Continuing", + "Ended", + "Unreleased" + ], + "type": "string", + "description": "The status of a series." + }, + "SeriesTimerCancelledMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/TimerEventInfo" + } + ], + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "SeriesTimerCancelled", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Series timer cancelled message." + }, + "SeriesTimerCreatedMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/TimerEventInfo" + } + ], + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "SeriesTimerCreated", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Series timer created message." + }, + "SeriesTimerInfoDto": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "description": "Gets or sets the Id of the recording.", + "nullable": true + }, + "Type": { + "type": "string", + "nullable": true + }, + "ServerId": { + "type": "string", + "description": "Gets or sets the server identifier.", + "nullable": true + }, + "ExternalId": { + "type": "string", + "description": "Gets or sets the external identifier.", + "nullable": true + }, + "ChannelId": { + "type": "string", + "description": "Gets or sets the channel id of the recording.", + "format": "uuid" + }, + "ExternalChannelId": { + "type": "string", + "description": "Gets or sets the external channel identifier.", + "nullable": true + }, + "ChannelName": { + "type": "string", + "description": "Gets or sets the channel name of the recording.", + "nullable": true + }, + "ChannelPrimaryImageTag": { + "type": "string", + "nullable": true + }, + "ProgramId": { + "type": "string", + "description": "Gets or sets the program identifier.", + "nullable": true + }, + "ExternalProgramId": { + "type": "string", + "description": "Gets or sets the external program identifier.", + "nullable": true + }, + "Name": { + "type": "string", + "description": "Gets or sets the name of the recording.", + "nullable": true + }, + "Overview": { + "type": "string", + "description": "Gets or sets the description of the recording.", + "nullable": true + }, + "StartDate": { + "type": "string", + "description": "Gets or sets the start date of the recording, in UTC.", + "format": "date-time" + }, + "EndDate": { + "type": "string", + "description": "Gets or sets the end date of the recording, in UTC.", + "format": "date-time" + }, + "ServiceName": { + "type": "string", + "description": "Gets or sets the name of the service.", + "nullable": true + }, + "Priority": { + "type": "integer", + "description": "Gets or sets the priority.", + "format": "int32" + }, + "PrePaddingSeconds": { + "type": "integer", + "description": "Gets or sets the pre padding seconds.", + "format": "int32" + }, + "PostPaddingSeconds": { + "type": "integer", + "description": "Gets or sets the post padding seconds.", + "format": "int32" + }, + "IsPrePaddingRequired": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is pre padding required." + }, + "ParentBackdropItemId": { + "type": "string", + "description": "Gets or sets the Id of the Parent that has a backdrop if the item does not have one.", + "nullable": true + }, + "ParentBackdropImageTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the parent backdrop image tags.", + "nullable": true + }, + "IsPostPaddingRequired": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is post padding required." + }, + "KeepUntil": { + "enum": [ + "UntilDeleted", + "UntilSpaceNeeded", + "UntilWatched", + "UntilDate" + ], + "allOf": [ + { + "$ref": "#/components/schemas/KeepUntil" + } + ] + }, + "RecordAnyTime": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [record any time]." + }, + "SkipEpisodesInLibrary": { + "type": "boolean" + }, + "RecordAnyChannel": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [record any channel]." + }, + "KeepUpTo": { + "type": "integer", + "format": "int32" + }, + "RecordNewOnly": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [record new only]." + }, + "Days": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DayOfWeek" + }, + "description": "Gets or sets the days.", + "nullable": true + }, + "DayPattern": { + "enum": [ + "Daily", + "Weekdays", + "Weekends" + ], + "allOf": [ + { + "$ref": "#/components/schemas/DayPattern" + } + ], + "description": "Gets or sets the day pattern.", + "nullable": true + }, + "ImageTags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Gets or sets the image tags.", + "nullable": true + }, + "ParentThumbItemId": { + "type": "string", + "description": "Gets or sets the parent thumb item id.", + "nullable": true + }, + "ParentThumbImageTag": { + "type": "string", + "description": "Gets or sets the parent thumb image tag.", + "nullable": true + }, + "ParentPrimaryImageItemId": { + "type": "string", + "description": "Gets or sets the parent primary image item identifier.", + "format": "uuid", + "nullable": true + }, + "ParentPrimaryImageTag": { + "type": "string", + "description": "Gets or sets the parent primary image tag.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class SeriesTimerInfoDto." + }, + "SeriesTimerInfoDtoQueryResult": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesTimerInfoDto" + }, + "description": "Gets or sets the items." + }, + "TotalRecordCount": { + "type": "integer", + "description": "Gets or sets the total number of records available.", + "format": "int32" + }, + "StartIndex": { + "type": "integer", + "description": "Gets or sets the index of the first record in Items.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Query result container." + }, + "ServerConfiguration": { + "type": "object", + "properties": { + "LogFileRetentionDays": { + "type": "integer", + "description": "Gets or sets the number of days we should retain log files.", + "format": "int32" + }, + "IsStartupWizardCompleted": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is first run." + }, + "CachePath": { + "type": "string", + "description": "Gets or sets the cache path.", + "nullable": true + }, + "PreviousVersion": { + "type": "string", + "description": "Gets or sets the last known version that was ran using the configuration.", + "nullable": true + }, + "PreviousVersionStr": { + "type": "string", + "description": "Gets or sets the stringified PreviousVersion to be stored/loaded,\r\nbecause System.Version itself isn't xml-serializable.", + "nullable": true + }, + "EnableMetrics": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to enable prometheus metrics exporting." + }, + "EnableNormalizedItemByNameIds": { + "type": "boolean" + }, + "IsPortAuthorized": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is port authorized." + }, + "QuickConnectAvailable": { + "type": "boolean", + "description": "Gets or sets a value indicating whether quick connect is available for use on this server." + }, + "EnableCaseSensitiveItemIds": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [enable case-sensitive item ids]." + }, + "DisableLiveTvChannelUserDataName": { + "type": "boolean" + }, + "MetadataPath": { + "type": "string", + "description": "Gets or sets the metadata path." + }, + "PreferredMetadataLanguage": { + "type": "string", + "description": "Gets or sets the preferred metadata language." + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code." + }, + "SortReplaceCharacters": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets characters to be replaced with a ' ' in strings to create a sort name." + }, + "SortRemoveCharacters": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets characters to be removed from strings to create a sort name." + }, + "SortRemoveWords": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets words to be removed from strings to create a sort name." + }, + "MinResumePct": { + "type": "integer", + "description": "Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.", + "format": "int32" + }, + "MaxResumePct": { + "type": "integer", + "description": "Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.", + "format": "int32" + }, + "MinResumeDurationSeconds": { + "type": "integer", + "description": "Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..", + "format": "int32" + }, + "MinAudiobookResume": { + "type": "integer", + "description": "Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated.", + "format": "int32" + }, + "MaxAudiobookResume": { + "type": "integer", + "description": "Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.", + "format": "int32" + }, + "InactiveSessionThreshold": { + "type": "integer", + "description": "Gets or sets the threshold in minutes after a inactive session gets closed automatically.\r\nIf set to 0 the check for inactive sessions gets disabled.", + "format": "int32" + }, + "LibraryMonitorDelay": { + "type": "integer", + "description": "Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed\r\nSome delay is necessary with some items because their creation is not atomic. It involves the creation of several\r\ndifferent directories and files.", + "format": "int32" + }, + "LibraryUpdateDuration": { + "type": "integer", + "description": "Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification.", + "format": "int32" + }, + "CacheSize": { + "type": "integer", + "description": "Gets or sets the maximum amount of items to cache.", + "format": "int32" + }, + "ImageSavingConvention": { + "enum": [ + "Legacy", + "Compatible" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageSavingConvention" + } + ], + "description": "Gets or sets the image saving convention." + }, + "MetadataOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataOptions" + } + }, + "SkipDeserializationForBasicTypes": { + "type": "boolean" + }, + "ServerName": { + "type": "string" + }, + "UICulture": { + "type": "string" + }, + "SaveMetadataHidden": { + "type": "boolean" + }, + "ContentTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NameValuePair" + } + }, + "RemoteClientBitrateLimit": { + "type": "integer", + "format": "int32" + }, + "EnableFolderView": { + "type": "boolean" + }, + "EnableGroupingMoviesIntoCollections": { + "type": "boolean" + }, + "EnableGroupingShowsIntoCollections": { + "type": "boolean" + }, + "DisplaySpecialsWithinSeasons": { + "type": "boolean" + }, + "CodecsUsed": { + "type": "array", + "items": { + "type": "string" + } + }, + "PluginRepositories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RepositoryInfo" + } + }, + "EnableExternalContentInSuggestions": { + "type": "boolean" + }, + "ImageExtractionTimeoutMs": { + "type": "integer", + "format": "int32" + }, + "PathSubstitutions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathSubstitution" + } + }, + "EnableSlowResponseWarning": { + "type": "boolean", + "description": "Gets or sets a value indicating whether slow server responses should be logged as a warning." + }, + "SlowResponseThresholdMs": { + "type": "integer", + "description": "Gets or sets the threshold for the slow response time warning in ms.", + "format": "int64" + }, + "CorsHosts": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the cors hosts." + }, + "ActivityLogRetentionDays": { + "type": "integer", + "description": "Gets or sets the number of days we should retain activity logs.", + "format": "int32", + "nullable": true + }, + "LibraryScanFanoutConcurrency": { + "type": "integer", + "description": "Gets or sets the how the library scan fans out.", + "format": "int32" + }, + "LibraryMetadataRefreshConcurrency": { + "type": "integer", + "description": "Gets or sets the how many metadata refreshes can run concurrently.", + "format": "int32" + }, + "AllowClientLogUpload": { + "type": "boolean", + "description": "Gets or sets a value indicating whether clients should be allowed to upload logs." + }, + "DummyChapterDuration": { + "type": "integer", + "description": "Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation altogether.", + "format": "int32" + }, + "ChapterImageResolution": { + "enum": [ + "MatchSource", + "P144", + "P240", + "P360", + "P480", + "P720", + "P1080", + "P1440", + "P2160" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ImageResolution" + } + ], + "description": "Gets or sets the chapter image resolution." + }, + "ParallelImageEncodingLimit": { + "type": "integer", + "description": "Gets or sets the limit for parallel image encoding.", + "format": "int32" + }, + "CastReceiverApplications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CastReceiverApplication" + }, + "description": "Gets or sets the list of cast receiver applications." + }, + "TrickplayOptions": { + "allOf": [ + { + "$ref": "#/components/schemas/TrickplayOptions" + } + ], + "description": "Gets or sets the trickplay options." + }, + "EnableLegacyAuthorization": { + "type": "boolean", + "description": "Gets or sets a value indicating whether old authorization methods are allowed." + } + }, + "additionalProperties": false, + "description": "Represents the server configuration." + }, + "ServerDiscoveryInfo": { + "type": "object", + "properties": { + "Address": { + "type": "string", + "description": "Gets the address." + }, + "Id": { + "type": "string", + "description": "Gets the server identifier." + }, + "Name": { + "type": "string", + "description": "Gets the name." + }, + "EndpointAddress": { + "type": "string", + "description": "Gets the endpoint address.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "The server discovery info model." + }, + "ServerRestartingMessage": { + "type": "object", + "properties": { + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "ServerRestarting", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Server restarting down message." + }, + "ServerShuttingDownMessage": { + "type": "object", + "properties": { + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "ServerShuttingDown", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Server shutting down message." + }, + "SessionInfoDto": { + "type": "object", + "properties": { + "PlayState": { + "allOf": [ + { + "$ref": "#/components/schemas/PlayerStateInfo" + } + ], + "description": "Gets or sets the play state.", + "nullable": true + }, + "AdditionalUsers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionUserInfo" + }, + "description": "Gets or sets the additional users.", + "nullable": true + }, + "Capabilities": { + "allOf": [ + { + "$ref": "#/components/schemas/ClientCapabilitiesDto" + } + ], + "description": "Gets or sets the client capabilities.", + "nullable": true + }, + "RemoteEndPoint": { + "type": "string", + "description": "Gets or sets the remote end point.", + "nullable": true + }, + "PlayableMediaTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaType" + }, + "description": "Gets or sets the playable media types." + }, + "Id": { + "type": "string", + "description": "Gets or sets the id.", + "nullable": true + }, + "UserId": { + "type": "string", + "description": "Gets or sets the user id.", + "format": "uuid" + }, + "UserName": { + "type": "string", + "description": "Gets or sets the username.", + "nullable": true + }, + "Client": { + "type": "string", + "description": "Gets or sets the type of the client.", + "nullable": true + }, + "LastActivityDate": { + "type": "string", + "description": "Gets or sets the last activity date.", + "format": "date-time" + }, + "LastPlaybackCheckIn": { + "type": "string", + "description": "Gets or sets the last playback check in.", + "format": "date-time" + }, + "LastPausedDate": { + "type": "string", + "description": "Gets or sets the last paused date.", + "format": "date-time", + "nullable": true + }, + "DeviceName": { + "type": "string", + "description": "Gets or sets the name of the device.", + "nullable": true + }, + "DeviceType": { + "type": "string", + "description": "Gets or sets the type of the device.", + "nullable": true + }, + "NowPlayingItem": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemDto" + } + ], + "description": "Gets or sets the now playing item.", + "nullable": true + }, + "NowViewingItem": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemDto" + } + ], + "description": "Gets or sets the now viewing item.", + "nullable": true + }, + "DeviceId": { + "type": "string", + "description": "Gets or sets the device id.", + "nullable": true + }, + "ApplicationVersion": { + "type": "string", + "description": "Gets or sets the application version.", + "nullable": true + }, + "TranscodingInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/TranscodingInfo" + } + ], + "description": "Gets or sets the transcoding info.", + "nullable": true + }, + "IsActive": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this session is active." + }, + "SupportsMediaControl": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the session supports media control." + }, + "SupportsRemoteControl": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the session supports remote control." + }, + "NowPlayingQueue": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueItem" + }, + "description": "Gets or sets the now playing queue.", + "nullable": true + }, + "NowPlayingQueueFullItems": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + }, + "description": "Gets or sets the now playing queue full items.", + "nullable": true + }, + "HasCustomDeviceName": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the session has a custom device name." + }, + "PlaylistItemId": { + "type": "string", + "description": "Gets or sets the playlist item id.", + "nullable": true + }, + "ServerId": { + "type": "string", + "description": "Gets or sets the server id.", + "nullable": true + }, + "UserPrimaryImageTag": { + "type": "string", + "description": "Gets or sets the user primary image tag.", + "nullable": true + }, + "SupportedCommands": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GeneralCommandType" + }, + "description": "Gets or sets the supported commands." + } + }, + "additionalProperties": false, + "description": "Session info DTO." + }, + "SessionMessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "type": "string", + "description": "The different kinds of messages that are used in the WebSocket api." + }, + "SessionsMessage": { + "type": "object", + "properties": { + "Data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionInfoDto" + }, + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "Sessions", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Sessions message." + }, + "SessionsStartMessage": { + "type": "object", + "properties": { + "Data": { + "type": "string", + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "SessionsStart", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Sessions start message.\r\nData is the timing data encoded as \"$initialDelay,$interval\" in ms." + }, + "SessionsStopMessage": { + "type": "object", + "properties": { + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "SessionsStop", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Sessions stop message." + }, + "SessionUserInfo": { + "type": "object", + "properties": { + "UserId": { + "type": "string", + "description": "Gets or sets the user identifier.", + "format": "uuid" + }, + "UserName": { + "type": "string", + "description": "Gets or sets the name of the user.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class SessionUserInfo." + }, + "SetChannelMappingDto": { + "required": [ + "ProviderChannelId", + "ProviderId", + "TunerChannelId" + ], + "type": "object", + "properties": { + "ProviderId": { + "type": "string", + "description": "Gets or sets the provider id." + }, + "TunerChannelId": { + "type": "string", + "description": "Gets or sets the tuner channel id." + }, + "ProviderChannelId": { + "type": "string", + "description": "Gets or sets the provider channel id." + } + }, + "additionalProperties": false, + "description": "Set channel mapping dto." + }, + "SetPlaylistItemRequestDto": { + "type": "object", + "properties": { + "PlaylistItemId": { + "type": "string", + "description": "Gets or sets the playlist identifier of the playing item.", + "format": "uuid" + } + }, + "additionalProperties": false, + "description": "Class SetPlaylistItemRequestDto." + }, + "SetRepeatModeRequestDto": { + "type": "object", + "properties": { + "Mode": { + "enum": [ + "RepeatOne", + "RepeatAll", + "RepeatNone" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupRepeatMode" + } + ], + "description": "Enum GroupRepeatMode." + } + }, + "additionalProperties": false, + "description": "Class SetRepeatModeRequestDto." + }, + "SetShuffleModeRequestDto": { + "type": "object", + "properties": { + "Mode": { + "enum": [ + "Sorted", + "Shuffle" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupShuffleMode" + } + ], + "description": "Enum GroupShuffleMode." + } + }, + "additionalProperties": false, + "description": "Class SetShuffleModeRequestDto." + }, + "SongInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "description": "Gets or sets the original title.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "MetadataLanguage": { + "type": "string", + "description": "Gets or sets the metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "Year": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "IsAutomated": { + "type": "boolean" + }, + "AlbumArtists": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "Album": { + "type": "string", + "nullable": true + }, + "Artists": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "SortOrder": { + "enum": [ + "Ascending", + "Descending" + ], + "type": "string", + "description": "An enum representing the sorting order." + }, + "SpecialViewOptionDto": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets view option name.", + "nullable": true + }, + "Id": { + "type": "string", + "description": "Gets or sets view option id.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Special view option dto." + }, + "StartupConfigurationDto": { + "type": "object", + "properties": { + "ServerName": { + "type": "string", + "description": "Gets or sets the server name.", + "nullable": true + }, + "UICulture": { + "type": "string", + "description": "Gets or sets UI language culture.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "PreferredMetadataLanguage": { + "type": "string", + "description": "Gets or sets the preferred language for the metadata.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "The startup configuration DTO." + }, + "StartupRemoteAccessDto": { + "required": [ + "EnableAutomaticPortMapping", + "EnableRemoteAccess" + ], + "type": "object", + "properties": { + "EnableRemoteAccess": { + "type": "boolean", + "description": "Gets or sets a value indicating whether enable remote access." + }, + "EnableAutomaticPortMapping": { + "type": "boolean", + "description": "Gets or sets a value indicating whether enable automatic port mapping.", + "deprecated": true + } + }, + "additionalProperties": false, + "description": "Startup remote access dto." + }, + "StartupUserDto": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the username.", + "nullable": true + }, + "Password": { + "type": "string", + "description": "Gets or sets the user's password.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "The startup user DTO." + }, + "SubtitleDeliveryMethod": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "type": "string", + "description": "Delivery method to use during playback of a specific subtitle format." + }, + "SubtitleOptions": { + "type": "object", + "properties": { + "SkipIfEmbeddedSubtitlesPresent": { + "type": "boolean" + }, + "SkipIfAudioTrackMatches": { + "type": "boolean" + }, + "DownloadLanguages": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "DownloadMovieSubtitles": { + "type": "boolean" + }, + "DownloadEpisodeSubtitles": { + "type": "boolean" + }, + "OpenSubtitlesUsername": { + "type": "string", + "nullable": true + }, + "OpenSubtitlesPasswordHash": { + "type": "string", + "nullable": true + }, + "IsOpenSubtitleVipAccount": { + "type": "boolean" + }, + "RequirePerfectMatch": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "SubtitlePlaybackMode": { + "enum": [ + "Default", + "Always", + "OnlyForced", + "None", + "Smart" + ], + "type": "string", + "description": "An enum representing a subtitle playback mode." + }, + "SubtitleProfile": { + "type": "object", + "properties": { + "Format": { + "type": "string", + "description": "Gets or sets the format.", + "nullable": true + }, + "Method": { + "enum": [ + "Encode", + "Embed", + "External", + "Hls", + "Drop" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitleDeliveryMethod" + } + ], + "description": "Gets or sets the delivery method." + }, + "DidlMode": { + "type": "string", + "description": "Gets or sets the DIDL mode.", + "nullable": true + }, + "Language": { + "type": "string", + "description": "Gets or sets the language.", + "nullable": true + }, + "Container": { + "type": "string", + "description": "Gets or sets the container.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "A class for subtitle profile information." + }, + "SyncPlayCommandMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/SendCommand" + } + ], + "description": "Class SendCommand.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "SyncPlayCommand", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Sync play command." + }, + "SyncPlayGroupDoesNotExistUpdate": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid", + "readOnly": true + }, + "Data": { + "type": "string", + "description": "Gets the update data.", + "readOnly": true + }, + "Type": { + "enum": [ + "UserJoined", + "UserLeft", + "GroupJoined", + "GroupLeft", + "StateUpdate", + "PlayQueue", + "NotInGroup", + "GroupDoesNotExist", + "LibraryAccessDenied" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupUpdateType" + } + ], + "description": "Enum GroupUpdateType.", + "default": "GroupDoesNotExist", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SyncPlayGroupJoinedUpdate": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid", + "readOnly": true + }, + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/GroupInfoDto" + } + ], + "description": "Gets the update data.", + "readOnly": true + }, + "Type": { + "enum": [ + "UserJoined", + "UserLeft", + "GroupJoined", + "GroupLeft", + "StateUpdate", + "PlayQueue", + "NotInGroup", + "GroupDoesNotExist", + "LibraryAccessDenied" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupUpdateType" + } + ], + "description": "Enum GroupUpdateType.", + "default": "GroupJoined", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SyncPlayGroupLeftUpdate": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid", + "readOnly": true + }, + "Data": { + "type": "string", + "description": "Gets the update data.", + "readOnly": true + }, + "Type": { + "enum": [ + "UserJoined", + "UserLeft", + "GroupJoined", + "GroupLeft", + "StateUpdate", + "PlayQueue", + "NotInGroup", + "GroupDoesNotExist", + "LibraryAccessDenied" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupUpdateType" + } + ], + "description": "Enum GroupUpdateType.", + "default": "GroupLeft", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SyncPlayGroupUpdateMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/GroupUpdate" + } + ], + "description": "Group update data" + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "SyncPlayGroupUpdate", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Untyped sync play command." + }, + "SyncPlayLibraryAccessDeniedUpdate": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid", + "readOnly": true + }, + "Data": { + "type": "string", + "description": "Gets the update data.", + "readOnly": true + }, + "Type": { + "enum": [ + "UserJoined", + "UserLeft", + "GroupJoined", + "GroupLeft", + "StateUpdate", + "PlayQueue", + "NotInGroup", + "GroupDoesNotExist", + "LibraryAccessDenied" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupUpdateType" + } + ], + "description": "Enum GroupUpdateType.", + "default": "LibraryAccessDenied", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SyncPlayNotInGroupUpdate": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid", + "readOnly": true + }, + "Data": { + "type": "string", + "description": "Gets the update data.", + "readOnly": true + }, + "Type": { + "enum": [ + "UserJoined", + "UserLeft", + "GroupJoined", + "GroupLeft", + "StateUpdate", + "PlayQueue", + "NotInGroup", + "GroupDoesNotExist", + "LibraryAccessDenied" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupUpdateType" + } + ], + "description": "Enum GroupUpdateType.", + "default": "NotInGroup", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SyncPlayPlayQueueUpdate": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid", + "readOnly": true + }, + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/PlayQueueUpdate" + } + ], + "description": "Gets the update data.", + "readOnly": true + }, + "Type": { + "enum": [ + "UserJoined", + "UserLeft", + "GroupJoined", + "GroupLeft", + "StateUpdate", + "PlayQueue", + "NotInGroup", + "GroupDoesNotExist", + "LibraryAccessDenied" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupUpdateType" + } + ], + "description": "Enum GroupUpdateType.", + "default": "PlayQueue", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SyncPlayQueueItem": { + "type": "object", + "properties": { + "ItemId": { + "type": "string", + "description": "Gets the item identifier.", + "format": "uuid" + }, + "PlaylistItemId": { + "type": "string", + "description": "Gets the playlist identifier of the item.", + "format": "uuid", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Class QueueItem." + }, + "SyncPlayStateUpdate": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid", + "readOnly": true + }, + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/GroupStateUpdate" + } + ], + "description": "Gets the update data.", + "readOnly": true + }, + "Type": { + "enum": [ + "UserJoined", + "UserLeft", + "GroupJoined", + "GroupLeft", + "StateUpdate", + "PlayQueue", + "NotInGroup", + "GroupDoesNotExist", + "LibraryAccessDenied" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupUpdateType" + } + ], + "description": "Enum GroupUpdateType.", + "default": "StateUpdate", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SyncPlayUserAccessType": { + "enum": [ + "CreateAndJoinGroups", + "JoinGroups", + "None" + ], + "type": "string", + "description": "Enum SyncPlayUserAccessType." + }, + "SyncPlayUserJoinedUpdate": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid", + "readOnly": true + }, + "Data": { + "type": "string", + "description": "Gets the update data.", + "readOnly": true + }, + "Type": { + "enum": [ + "UserJoined", + "UserLeft", + "GroupJoined", + "GroupLeft", + "StateUpdate", + "PlayQueue", + "NotInGroup", + "GroupDoesNotExist", + "LibraryAccessDenied" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupUpdateType" + } + ], + "description": "Enum GroupUpdateType.", + "default": "UserJoined", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SyncPlayUserLeftUpdate": { + "type": "object", + "properties": { + "GroupId": { + "type": "string", + "description": "Gets the group identifier.", + "format": "uuid", + "readOnly": true + }, + "Data": { + "type": "string", + "description": "Gets the update data.", + "readOnly": true + }, + "Type": { + "enum": [ + "UserJoined", + "UserLeft", + "GroupJoined", + "GroupLeft", + "StateUpdate", + "PlayQueue", + "NotInGroup", + "GroupDoesNotExist", + "LibraryAccessDenied" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GroupUpdateType" + } + ], + "description": "Enum GroupUpdateType.", + "default": "UserLeft", + "readOnly": true + } + }, + "additionalProperties": false + }, + "SystemInfo": { + "type": "object", + "properties": { + "LocalAddress": { + "type": "string", + "description": "Gets or sets the local address.", + "nullable": true + }, + "ServerName": { + "type": "string", + "description": "Gets or sets the name of the server.", + "nullable": true + }, + "Version": { + "type": "string", + "description": "Gets or sets the server version.", + "nullable": true + }, + "ProductName": { + "type": "string", + "description": "Gets or sets the product name. This is the AssemblyProduct name.", + "nullable": true + }, + "OperatingSystem": { + "type": "string", + "description": "Gets or sets the operating system.", + "nullable": true, + "deprecated": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the id.", + "nullable": true + }, + "StartupWizardCompleted": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the startup wizard is completed.", + "nullable": true + }, + "OperatingSystemDisplayName": { + "type": "string", + "description": "Gets or sets the display name of the operating system.", + "nullable": true, + "deprecated": true + }, + "PackageName": { + "type": "string", + "description": "Gets or sets the package name.", + "nullable": true + }, + "HasPendingRestart": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance has pending restart." + }, + "IsShuttingDown": { + "type": "boolean" + }, + "SupportsLibraryMonitor": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [supports library monitor]." + }, + "WebSocketPortNumber": { + "type": "integer", + "description": "Gets or sets the web socket port number.", + "format": "int32" + }, + "CompletedInstallations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InstallationInfo" + }, + "description": "Gets or sets the completed installations.", + "nullable": true + }, + "CanSelfRestart": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance can self restart.", + "default": true, + "deprecated": true + }, + "CanLaunchWebBrowser": { + "type": "boolean", + "default": false, + "deprecated": true + }, + "ProgramDataPath": { + "type": "string", + "description": "Gets or sets the program data path.", + "nullable": true, + "deprecated": true + }, + "WebPath": { + "type": "string", + "description": "Gets or sets the web UI resources path.", + "nullable": true, + "deprecated": true + }, + "ItemsByNamePath": { + "type": "string", + "description": "Gets or sets the items by name path.", + "nullable": true, + "deprecated": true + }, + "CachePath": { + "type": "string", + "description": "Gets or sets the cache path.", + "nullable": true, + "deprecated": true + }, + "LogPath": { + "type": "string", + "description": "Gets or sets the log path.", + "nullable": true, + "deprecated": true + }, + "InternalMetadataPath": { + "type": "string", + "description": "Gets or sets the internal metadata path.", + "nullable": true, + "deprecated": true + }, + "TranscodingTempPath": { + "type": "string", + "description": "Gets or sets the transcode path.", + "nullable": true, + "deprecated": true + }, + "CastReceiverApplications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CastReceiverApplication" + }, + "description": "Gets or sets the list of cast receiver applications.", + "nullable": true + }, + "HasUpdateAvailable": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance has update available.", + "default": false, + "deprecated": true + }, + "EncoderLocation": { + "type": "string", + "default": "System", + "nullable": true, + "deprecated": true + }, + "SystemArchitecture": { + "type": "string", + "default": "X64", + "nullable": true, + "deprecated": true + } + }, + "additionalProperties": false, + "description": "Class SystemInfo." + }, + "SystemStorageDto": { + "type": "object", + "properties": { + "ProgramDataFolder": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderStorageDto" + } + ], + "description": "Gets or sets the Storage information of the program data folder." + }, + "WebFolder": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderStorageDto" + } + ], + "description": "Gets or sets the Storage information of the web UI resources folder." + }, + "ImageCacheFolder": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderStorageDto" + } + ], + "description": "Gets or sets the Storage information of the folder where images are cached." + }, + "CacheFolder": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderStorageDto" + } + ], + "description": "Gets or sets the Storage information of the cache folder." + }, + "LogFolder": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderStorageDto" + } + ], + "description": "Gets or sets the Storage information of the folder where logfiles are saved to." + }, + "InternalMetadataFolder": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderStorageDto" + } + ], + "description": "Gets or sets the Storage information of the folder where metadata is stored." + }, + "TranscodingTempFolder": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderStorageDto" + } + ], + "description": "Gets or sets the Storage information of the transcoding cache." + }, + "Libraries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryStorageDto" + }, + "description": "Gets or sets the storage informations of all libraries." + } + }, + "additionalProperties": false, + "description": "Contains informations about the systems storage." + }, + "TaskCompletionStatus": { + "enum": [ + "Completed", + "Failed", + "Cancelled", + "Aborted" + ], + "type": "string", + "description": "Enum TaskCompletionStatus." + }, + "TaskInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "State": { + "enum": [ + "Idle", + "Cancelling", + "Running" + ], + "allOf": [ + { + "$ref": "#/components/schemas/TaskState" + } + ], + "description": "Gets or sets the state of the task." + }, + "CurrentProgressPercentage": { + "type": "number", + "description": "Gets or sets the progress.", + "format": "double", + "nullable": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the id.", + "nullable": true + }, + "LastExecutionResult": { + "allOf": [ + { + "$ref": "#/components/schemas/TaskResult" + } + ], + "description": "Gets or sets the last execution result.", + "nullable": true + }, + "Triggers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskTriggerInfo" + }, + "description": "Gets or sets the triggers.", + "nullable": true + }, + "Description": { + "type": "string", + "description": "Gets or sets the description.", + "nullable": true + }, + "Category": { + "type": "string", + "description": "Gets or sets the category.", + "nullable": true + }, + "IsHidden": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is hidden." + }, + "Key": { + "type": "string", + "description": "Gets or sets the key.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class TaskInfo." + }, + "TaskResult": { + "type": "object", + "properties": { + "StartTimeUtc": { + "type": "string", + "description": "Gets or sets the start time UTC.", + "format": "date-time" + }, + "EndTimeUtc": { + "type": "string", + "description": "Gets or sets the end time UTC.", + "format": "date-time" + }, + "Status": { + "enum": [ + "Completed", + "Failed", + "Cancelled", + "Aborted" + ], + "allOf": [ + { + "$ref": "#/components/schemas/TaskCompletionStatus" + } + ], + "description": "Gets or sets the status." + }, + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "Key": { + "type": "string", + "description": "Gets or sets the key.", + "nullable": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the id.", + "nullable": true + }, + "ErrorMessage": { + "type": "string", + "description": "Gets or sets the error message.", + "nullable": true + }, + "LongErrorMessage": { + "type": "string", + "description": "Gets or sets the long error message.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class TaskExecutionInfo." + }, + "TaskState": { + "enum": [ + "Idle", + "Cancelling", + "Running" + ], + "type": "string", + "description": "Enum TaskState." + }, + "TaskTriggerInfo": { + "type": "object", + "properties": { + "Type": { + "enum": [ + "DailyTrigger", + "WeeklyTrigger", + "IntervalTrigger", + "StartupTrigger" + ], + "allOf": [ + { + "$ref": "#/components/schemas/TaskTriggerInfoType" + } + ], + "description": "Gets or sets the type." + }, + "TimeOfDayTicks": { + "type": "integer", + "description": "Gets or sets the time of day.", + "format": "int64", + "nullable": true + }, + "IntervalTicks": { + "type": "integer", + "description": "Gets or sets the interval.", + "format": "int64", + "nullable": true + }, + "DayOfWeek": { + "enum": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "allOf": [ + { + "$ref": "#/components/schemas/DayOfWeek" + } + ], + "description": "Gets or sets the day of week.", + "nullable": true + }, + "MaxRuntimeTicks": { + "type": "integer", + "description": "Gets or sets the maximum runtime ticks.", + "format": "int64", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class TaskTriggerInfo." + }, + "TaskTriggerInfoType": { + "enum": [ + "DailyTrigger", + "WeeklyTrigger", + "IntervalTrigger", + "StartupTrigger" + ], + "type": "string", + "description": "Enum TaskTriggerInfoType." + }, + "ThemeMediaResult": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseItemDto" + }, + "description": "Gets or sets the items." + }, + "TotalRecordCount": { + "type": "integer", + "description": "Gets or sets the total number of records available.", + "format": "int32" + }, + "StartIndex": { + "type": "integer", + "description": "Gets or sets the index of the first record in Items.", + "format": "int32" + }, + "OwnerId": { + "type": "string", + "description": "Gets or sets the owner id.", + "format": "uuid" + } + }, + "additionalProperties": false, + "description": "Class ThemeMediaResult." + }, + "TimerCancelledMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/TimerEventInfo" + } + ], + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "TimerCancelled", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Timer cancelled message." + }, + "TimerCreatedMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/TimerEventInfo" + } + ], + "description": "Gets or sets the data.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "TimerCreated", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Timer created message." + }, + "TimerEventInfo": { + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "ProgramId": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, + "TimerInfoDto": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "description": "Gets or sets the Id of the recording.", + "nullable": true + }, + "Type": { + "type": "string", + "nullable": true + }, + "ServerId": { + "type": "string", + "description": "Gets or sets the server identifier.", + "nullable": true + }, + "ExternalId": { + "type": "string", + "description": "Gets or sets the external identifier.", + "nullable": true + }, + "ChannelId": { + "type": "string", + "description": "Gets or sets the channel id of the recording.", + "format": "uuid" + }, + "ExternalChannelId": { + "type": "string", + "description": "Gets or sets the external channel identifier.", + "nullable": true + }, + "ChannelName": { + "type": "string", + "description": "Gets or sets the channel name of the recording.", + "nullable": true + }, + "ChannelPrimaryImageTag": { + "type": "string", + "nullable": true + }, + "ProgramId": { + "type": "string", + "description": "Gets or sets the program identifier.", + "nullable": true + }, + "ExternalProgramId": { + "type": "string", + "description": "Gets or sets the external program identifier.", + "nullable": true + }, + "Name": { + "type": "string", + "description": "Gets or sets the name of the recording.", + "nullable": true + }, + "Overview": { + "type": "string", + "description": "Gets or sets the description of the recording.", + "nullable": true + }, + "StartDate": { + "type": "string", + "description": "Gets or sets the start date of the recording, in UTC.", + "format": "date-time" + }, + "EndDate": { + "type": "string", + "description": "Gets or sets the end date of the recording, in UTC.", + "format": "date-time" + }, + "ServiceName": { + "type": "string", + "description": "Gets or sets the name of the service.", + "nullable": true + }, + "Priority": { + "type": "integer", + "description": "Gets or sets the priority.", + "format": "int32" + }, + "PrePaddingSeconds": { + "type": "integer", + "description": "Gets or sets the pre padding seconds.", + "format": "int32" + }, + "PostPaddingSeconds": { + "type": "integer", + "description": "Gets or sets the post padding seconds.", + "format": "int32" + }, + "IsPrePaddingRequired": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is pre padding required." + }, + "ParentBackdropItemId": { + "type": "string", + "description": "Gets or sets the Id of the Parent that has a backdrop if the item does not have one.", + "nullable": true + }, + "ParentBackdropImageTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the parent backdrop image tags.", + "nullable": true + }, + "IsPostPaddingRequired": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is post padding required." + }, + "KeepUntil": { + "enum": [ + "UntilDeleted", + "UntilSpaceNeeded", + "UntilWatched", + "UntilDate" + ], + "allOf": [ + { + "$ref": "#/components/schemas/KeepUntil" + } + ] + }, + "Status": { + "enum": [ + "New", + "InProgress", + "Completed", + "Cancelled", + "ConflictedOk", + "ConflictedNotOk", + "Error" + ], + "allOf": [ + { + "$ref": "#/components/schemas/RecordingStatus" + } + ], + "description": "Gets or sets the status." + }, + "SeriesTimerId": { + "type": "string", + "description": "Gets or sets the series timer identifier.", + "nullable": true + }, + "ExternalSeriesTimerId": { + "type": "string", + "description": "Gets or sets the external series timer identifier.", + "nullable": true + }, + "RunTimeTicks": { + "type": "integer", + "description": "Gets or sets the run time ticks.", + "format": "int64", + "nullable": true + }, + "ProgramInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseItemDto" + } + ], + "description": "Gets or sets the program information.", + "nullable": true + } + }, + "additionalProperties": false + }, + "TimerInfoDtoQueryResult": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimerInfoDto" + }, + "description": "Gets or sets the items." + }, + "TotalRecordCount": { + "type": "integer", + "description": "Gets or sets the total number of records available.", + "format": "int32" + }, + "StartIndex": { + "type": "integer", + "description": "Gets or sets the index of the first record in Items.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Query result container." + }, + "TonemappingAlgorithm": { + "enum": [ + "none", + "clip", + "linear", + "gamma", + "reinhard", + "hable", + "mobius", + "bt2390" + ], + "type": "string", + "description": "Enum containing tonemapping algorithms." + }, + "TonemappingMode": { + "enum": [ + "auto", + "max", + "rgb", + "lum", + "itp" + ], + "type": "string", + "description": "Enum containing tonemapping modes." + }, + "TonemappingRange": { + "enum": [ + "auto", + "tv", + "pc" + ], + "type": "string", + "description": "Enum containing tonemapping ranges." + }, + "TrailerInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "OriginalTitle": { + "type": "string", + "description": "Gets or sets the original title.", + "nullable": true + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "MetadataLanguage": { + "type": "string", + "description": "Gets or sets the metadata language.", + "nullable": true + }, + "MetadataCountryCode": { + "type": "string", + "description": "Gets or sets the metadata country code.", + "nullable": true + }, + "ProviderIds": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "description": "Gets or sets the provider ids.", + "nullable": true + }, + "Year": { + "type": "integer", + "description": "Gets or sets the year.", + "format": "int32", + "nullable": true + }, + "IndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "ParentIndexNumber": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "PremiereDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "IsAutomated": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "TrailerInfoRemoteSearchQuery": { + "type": "object", + "properties": { + "SearchInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/TrailerInfo" + } + ], + "nullable": true + }, + "ItemId": { + "type": "string", + "format": "uuid" + }, + "SearchProviderName": { + "type": "string", + "description": "Gets or sets the provider name to search within if set.", + "nullable": true + }, + "IncludeDisabledProviders": { + "type": "boolean", + "description": "Gets or sets a value indicating whether disabled providers should be included." + } + }, + "additionalProperties": false + }, + "TranscodeReason": { + "enum": [ + "ContainerNotSupported", + "VideoCodecNotSupported", + "AudioCodecNotSupported", + "SubtitleCodecNotSupported", + "AudioIsExternal", + "SecondaryAudioNotSupported", + "VideoProfileNotSupported", + "VideoLevelNotSupported", + "VideoResolutionNotSupported", + "VideoBitDepthNotSupported", + "VideoFramerateNotSupported", + "RefFramesNotSupported", + "AnamorphicVideoNotSupported", + "InterlacedVideoNotSupported", + "AudioChannelsNotSupported", + "AudioProfileNotSupported", + "AudioSampleRateNotSupported", + "AudioBitDepthNotSupported", + "ContainerBitrateExceedsLimit", + "VideoBitrateNotSupported", + "AudioBitrateNotSupported", + "UnknownVideoStreamInfo", + "UnknownAudioStreamInfo", + "DirectPlayError", + "VideoRangeTypeNotSupported", + "VideoCodecTagNotSupported", + "StreamCountExceedsLimit" + ], + "type": "string" + }, + "TranscodeSeekInfo": { + "enum": [ + "Auto", + "Bytes" + ], + "type": "string" + }, + "TranscodingInfo": { + "type": "object", + "properties": { + "AudioCodec": { + "type": "string", + "description": "Gets or sets the thread count used for encoding.", + "nullable": true + }, + "VideoCodec": { + "type": "string", + "description": "Gets or sets the thread count used for encoding.", + "nullable": true + }, + "Container": { + "type": "string", + "description": "Gets or sets the thread count used for encoding.", + "nullable": true + }, + "IsVideoDirect": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the video is passed through." + }, + "IsAudioDirect": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the audio is passed through." + }, + "Bitrate": { + "type": "integer", + "description": "Gets or sets the bitrate.", + "format": "int32", + "nullable": true + }, + "Framerate": { + "type": "number", + "description": "Gets or sets the framerate.", + "format": "float", + "nullable": true + }, + "CompletionPercentage": { + "type": "number", + "description": "Gets or sets the completion percentage.", + "format": "double", + "nullable": true + }, + "Width": { + "type": "integer", + "description": "Gets or sets the video width.", + "format": "int32", + "nullable": true + }, + "Height": { + "type": "integer", + "description": "Gets or sets the video height.", + "format": "int32", + "nullable": true + }, + "AudioChannels": { + "type": "integer", + "description": "Gets or sets the audio channels.", + "format": "int32", + "nullable": true + }, + "HardwareAccelerationType": { + "enum": [ + "none", + "amf", + "qsv", + "nvenc", + "v4l2m2m", + "vaapi", + "videotoolbox", + "rkmpp" + ], + "allOf": [ + { + "$ref": "#/components/schemas/HardwareAccelerationType" + } + ], + "description": "Gets or sets the hardware acceleration type.", + "nullable": true + }, + "TranscodeReasons": { + "enum": [ + "ContainerNotSupported", + "VideoCodecNotSupported", + "AudioCodecNotSupported", + "SubtitleCodecNotSupported", + "AudioIsExternal", + "SecondaryAudioNotSupported", + "VideoProfileNotSupported", + "VideoLevelNotSupported", + "VideoResolutionNotSupported", + "VideoBitDepthNotSupported", + "VideoFramerateNotSupported", + "RefFramesNotSupported", + "AnamorphicVideoNotSupported", + "InterlacedVideoNotSupported", + "AudioChannelsNotSupported", + "AudioProfileNotSupported", + "AudioSampleRateNotSupported", + "AudioBitDepthNotSupported", + "ContainerBitrateExceedsLimit", + "VideoBitrateNotSupported", + "AudioBitrateNotSupported", + "UnknownVideoStreamInfo", + "UnknownAudioStreamInfo", + "DirectPlayError", + "VideoRangeTypeNotSupported", + "VideoCodecTagNotSupported", + "StreamCountExceedsLimit" + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/TranscodeReason" + }, + "description": "Gets or sets the transcode reasons." + } + }, + "additionalProperties": false, + "description": "Class holding information on a running transcode." + }, + "TranscodingProfile": { + "type": "object", + "properties": { + "Container": { + "type": "string", + "description": "Gets or sets the container." + }, + "Type": { + "enum": [ + "Audio", + "Video", + "Photo", + "Subtitle", + "Lyric" + ], + "allOf": [ + { + "$ref": "#/components/schemas/DlnaProfileType" + } + ], + "description": "Gets or sets the DLNA profile type." + }, + "VideoCodec": { + "type": "string", + "description": "Gets or sets the video codec." + }, + "AudioCodec": { + "type": "string", + "description": "Gets or sets the audio codec." + }, + "Protocol": { + "enum": [ + "http", + "hls" + ], + "allOf": [ + { + "$ref": "#/components/schemas/MediaStreamProtocol" + } + ], + "description": "Gets or sets the protocol." + }, + "EstimateContentLength": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the content length should be estimated.", + "default": false + }, + "EnableMpegtsM2TsMode": { + "type": "boolean", + "description": "Gets or sets a value indicating whether M2TS mode is enabled.", + "default": false + }, + "TranscodeSeekInfo": { + "enum": [ + "Auto", + "Bytes" + ], + "allOf": [ + { + "$ref": "#/components/schemas/TranscodeSeekInfo" + } + ], + "description": "Gets or sets the transcoding seek info mode.", + "default": "Auto" + }, + "CopyTimestamps": { + "type": "boolean", + "description": "Gets or sets a value indicating whether timestamps should be copied.", + "default": false + }, + "Context": { + "enum": [ + "Streaming", + "Static" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EncodingContext" + } + ], + "description": "Gets or sets the encoding context.", + "default": "Streaming" + }, + "EnableSubtitlesInManifest": { + "type": "boolean", + "description": "Gets or sets a value indicating whether subtitles are allowed in the manifest.", + "default": false + }, + "MaxAudioChannels": { + "type": "string", + "description": "Gets or sets the maximum audio channels.", + "nullable": true + }, + "MinSegments": { + "type": "integer", + "description": "Gets or sets the minimum amount of segments.", + "format": "int32", + "default": 0 + }, + "SegmentLength": { + "type": "integer", + "description": "Gets or sets the segment length.", + "format": "int32", + "default": 0 + }, + "BreakOnNonKeyFrames": { + "type": "boolean", + "description": "Gets or sets a value indicating whether breaking the video stream on non-keyframes is supported.", + "default": false + }, + "Conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProfileCondition" + }, + "description": "Gets or sets the profile conditions." + }, + "EnableAudioVbrEncoding": { + "type": "boolean", + "description": "Gets or sets a value indicating whether variable bitrate encoding is supported.", + "default": true + } + }, + "additionalProperties": false, + "description": "A class for transcoding profile information.\r\nNote for client developers: Conditions defined in MediaBrowser.Model.Dlna.CodecProfile has higher priority and can override values defined here." + }, + "TransportStreamTimestamp": { + "enum": [ + "None", + "Zero", + "Valid" + ], + "type": "string" + }, + "TrickplayInfoDto": { + "type": "object", + "properties": { + "Width": { + "type": "integer", + "description": "Gets the width of an individual thumbnail.", + "format": "int32" + }, + "Height": { + "type": "integer", + "description": "Gets the height of an individual thumbnail.", + "format": "int32" + }, + "TileWidth": { + "type": "integer", + "description": "Gets the amount of thumbnails per row.", + "format": "int32" + }, + "TileHeight": { + "type": "integer", + "description": "Gets the amount of thumbnails per column.", + "format": "int32" + }, + "ThumbnailCount": { + "type": "integer", + "description": "Gets the total amount of non-black thumbnails.", + "format": "int32" + }, + "Interval": { + "type": "integer", + "description": "Gets the interval in milliseconds between each trickplay thumbnail.", + "format": "int32" + }, + "Bandwidth": { + "type": "integer", + "description": "Gets the peak bandwidth usage in bits per second.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "The trickplay api model." + }, + "TrickplayOptions": { + "type": "object", + "properties": { + "EnableHwAcceleration": { + "type": "boolean", + "description": "Gets or sets a value indicating whether or not to use HW acceleration." + }, + "EnableHwEncoding": { + "type": "boolean", + "description": "Gets or sets a value indicating whether or not to use HW accelerated MJPEG encoding." + }, + "EnableKeyFrameOnlyExtraction": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to only extract key frames.\r\nSignificantly faster, but is not compatible with all decoders and/or video files." + }, + "ScanBehavior": { + "enum": [ + "Blocking", + "NonBlocking" + ], + "allOf": [ + { + "$ref": "#/components/schemas/TrickplayScanBehavior" + } + ], + "description": "Gets or sets the behavior used by trickplay provider on library scan/update." + }, + "ProcessPriority": { + "enum": [ + "Normal", + "Idle", + "High", + "RealTime", + "BelowNormal", + "AboveNormal" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ProcessPriorityClass" + } + ], + "description": "Gets or sets the process priority for the ffmpeg process." + }, + "Interval": { + "type": "integer", + "description": "Gets or sets the interval, in ms, between each new trickplay image.", + "format": "int32" + }, + "WidthResolutions": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "description": "Gets or sets the target width resolutions, in px, to generates preview images for." + }, + "TileWidth": { + "type": "integer", + "description": "Gets or sets number of tile images to allow in X dimension.", + "format": "int32" + }, + "TileHeight": { + "type": "integer", + "description": "Gets or sets number of tile images to allow in Y dimension.", + "format": "int32" + }, + "Qscale": { + "type": "integer", + "description": "Gets or sets the ffmpeg output quality level.", + "format": "int32" + }, + "JpegQuality": { + "type": "integer", + "description": "Gets or sets the jpeg quality to use for image tiles.", + "format": "int32" + }, + "ProcessThreads": { + "type": "integer", + "description": "Gets or sets the number of threads to be used by ffmpeg.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Class TrickplayOptions." + }, + "TrickplayScanBehavior": { + "enum": [ + "Blocking", + "NonBlocking" + ], + "type": "string", + "description": "Enum TrickplayScanBehavior." + }, + "TunerChannelMapping": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "nullable": true + }, + "ProviderChannelName": { + "type": "string", + "nullable": true + }, + "ProviderChannelId": { + "type": "string", + "nullable": true + }, + "Id": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "TunerHostInfo": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "nullable": true + }, + "Url": { + "type": "string", + "nullable": true + }, + "Type": { + "type": "string", + "nullable": true + }, + "DeviceId": { + "type": "string", + "nullable": true + }, + "FriendlyName": { + "type": "string", + "nullable": true + }, + "ImportFavoritesOnly": { + "type": "boolean" + }, + "AllowHWTranscoding": { + "type": "boolean" + }, + "AllowFmp4TranscodingContainer": { + "type": "boolean" + }, + "AllowStreamSharing": { + "type": "boolean" + }, + "FallbackMaxStreamingBitrate": { + "type": "integer", + "format": "int32" + }, + "EnableStreamLooping": { + "type": "boolean" + }, + "Source": { + "type": "string", + "nullable": true + }, + "TunerCount": { + "type": "integer", + "format": "int32" + }, + "UserAgent": { + "type": "string", + "nullable": true + }, + "IgnoreDts": { + "type": "boolean" + }, + "ReadAtNativeFramerate": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "TypeOptions": { + "type": "object", + "properties": { + "Type": { + "type": "string", + "nullable": true + }, + "MetadataFetchers": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "MetadataFetcherOrder": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "ImageFetchers": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "ImageFetcherOrder": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "ImageOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageOption" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "UnratedItem": { + "enum": [ + "Movie", + "Trailer", + "Series", + "Music", + "Book", + "LiveTvChannel", + "LiveTvProgram", + "ChannelContent", + "Other" + ], + "type": "string", + "description": "An enum representing an unrated item." + }, + "UpdateLibraryOptionsDto": { + "type": "object", + "properties": { + "Id": { + "type": "string", + "description": "Gets or sets the library item id.", + "format": "uuid" + }, + "LibraryOptions": { + "allOf": [ + { + "$ref": "#/components/schemas/LibraryOptions" + } + ], + "description": "Gets or sets library options.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Update library options dto." + }, + "UpdateMediaPathRequestDto": { + "required": [ + "Name", + "PathInfo" + ], + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the library name." + }, + "PathInfo": { + "allOf": [ + { + "$ref": "#/components/schemas/MediaPathInfo" + } + ], + "description": "Gets or sets library folder path information." + } + }, + "additionalProperties": false, + "description": "Update library options dto." + }, + "UpdatePlaylistDto": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name of the new playlist.", + "nullable": true + }, + "Ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Gets or sets item ids of the playlist.", + "nullable": true + }, + "Users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlaylistUserPermissions" + }, + "description": "Gets or sets the playlist users.", + "nullable": true + }, + "IsPublic": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the playlist is public.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Update existing playlist dto. Fields set to `null` will not be updated and keep their current values." + }, + "UpdatePlaylistUserDto": { + "type": "object", + "properties": { + "CanEdit": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the user can edit the playlist.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values." + }, + "UpdateUserItemDataDto": { + "type": "object", + "properties": { + "Rating": { + "type": "number", + "description": "Gets or sets the rating.", + "format": "double", + "nullable": true + }, + "PlayedPercentage": { + "type": "number", + "description": "Gets or sets the played percentage.", + "format": "double", + "nullable": true + }, + "UnplayedItemCount": { + "type": "integer", + "description": "Gets or sets the unplayed item count.", + "format": "int32", + "nullable": true + }, + "PlaybackPositionTicks": { + "type": "integer", + "description": "Gets or sets the playback position ticks.", + "format": "int64", + "nullable": true + }, + "PlayCount": { + "type": "integer", + "description": "Gets or sets the play count.", + "format": "int32", + "nullable": true + }, + "IsFavorite": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is favorite.", + "nullable": true + }, + "Likes": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Dto.UpdateUserItemDataDto is likes.", + "nullable": true + }, + "LastPlayedDate": { + "type": "string", + "description": "Gets or sets the last played date.", + "format": "date-time", + "nullable": true + }, + "Played": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Dto.UserItemDataDto is played.", + "nullable": true + }, + "Key": { + "type": "string", + "description": "Gets or sets the key.", + "nullable": true + }, + "ItemId": { + "type": "string", + "description": "Gets or sets the item identifier.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "This is used by the api to get information about a item user data." + }, + "UpdateUserPassword": { + "type": "object", + "properties": { + "CurrentPassword": { + "type": "string", + "description": "Gets or sets the current sha1-hashed password.", + "nullable": true + }, + "CurrentPw": { + "type": "string", + "description": "Gets or sets the current plain text password.", + "nullable": true + }, + "NewPw": { + "type": "string", + "description": "Gets or sets the new plain text password.", + "nullable": true + }, + "ResetPassword": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to reset the password." + } + }, + "additionalProperties": false, + "description": "The update user password request body." + }, + "UploadSubtitleDto": { + "required": [ + "Data", + "Format", + "IsForced", + "IsHearingImpaired", + "Language" + ], + "type": "object", + "properties": { + "Language": { + "type": "string", + "description": "Gets or sets the subtitle language." + }, + "Format": { + "type": "string", + "description": "Gets or sets the subtitle format." + }, + "IsForced": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the subtitle is forced." + }, + "IsHearingImpaired": { + "type": "boolean", + "description": "Gets or sets a value indicating whether the subtitle is for hearing impaired." + }, + "Data": { + "type": "string", + "description": "Gets or sets the subtitle data." + } + }, + "additionalProperties": false, + "description": "Upload subtitles dto." + }, + "UserConfiguration": { + "type": "object", + "properties": { + "AudioLanguagePreference": { + "type": "string", + "description": "Gets or sets the audio language preference.", + "nullable": true + }, + "PlayDefaultAudioTrack": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [play default audio track]." + }, + "SubtitleLanguagePreference": { + "type": "string", + "description": "Gets or sets the subtitle language preference.", + "nullable": true + }, + "DisplayMissingEpisodes": { + "type": "boolean" + }, + "GroupedFolders": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "SubtitleMode": { + "enum": [ + "Default", + "Always", + "OnlyForced", + "None", + "Smart" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SubtitlePlaybackMode" + } + ], + "description": "An enum representing a subtitle playback mode." + }, + "DisplayCollectionsView": { + "type": "boolean" + }, + "EnableLocalPassword": { + "type": "boolean" + }, + "OrderedViews": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "LatestItemsExcludes": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "MyMediaExcludes": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "HidePlayedInLatest": { + "type": "boolean" + }, + "RememberAudioSelections": { + "type": "boolean" + }, + "RememberSubtitleSelections": { + "type": "boolean" + }, + "EnableNextEpisodeAutoPlay": { + "type": "boolean" + }, + "CastReceiverId": { + "type": "string", + "description": "Gets or sets the id of the selected cast receiver.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class UserConfiguration." + }, + "UserDataChangedMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/UserDataChangeInfo" + } + ], + "description": "Class UserDataChangeInfo.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "UserDataChanged", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "User data changed message." + }, + "UserDataChangeInfo": { + "type": "object", + "properties": { + "UserId": { + "type": "string", + "description": "Gets or sets the user id.", + "format": "uuid" + }, + "UserDataList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserItemDataDto" + }, + "description": "Gets or sets the user data list." + } + }, + "additionalProperties": false, + "description": "Class UserDataChangeInfo." + }, + "UserDeletedMessage": { + "type": "object", + "properties": { + "Data": { + "type": "string", + "description": "Gets or sets the data.", + "format": "uuid" + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "UserDeleted", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "User deleted message." + }, + "UserDto": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "ServerId": { + "type": "string", + "description": "Gets or sets the server identifier.", + "nullable": true + }, + "ServerName": { + "type": "string", + "description": "Gets or sets the name of the server.\r\nThis is not used by the server and is for client-side usage only.", + "nullable": true + }, + "Id": { + "type": "string", + "description": "Gets or sets the id.", + "format": "uuid" + }, + "PrimaryImageTag": { + "type": "string", + "description": "Gets or sets the primary image tag.", + "nullable": true + }, + "HasPassword": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance has password." + }, + "HasConfiguredPassword": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance has configured password." + }, + "HasConfiguredEasyPassword": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance has configured easy password.", + "deprecated": true + }, + "EnableAutoLogin": { + "type": "boolean", + "description": "Gets or sets whether async login is enabled or not.", + "nullable": true + }, + "LastLoginDate": { + "type": "string", + "description": "Gets or sets the last login date.", + "format": "date-time", + "nullable": true + }, + "LastActivityDate": { + "type": "string", + "description": "Gets or sets the last activity date.", + "format": "date-time", + "nullable": true + }, + "Configuration": { + "allOf": [ + { + "$ref": "#/components/schemas/UserConfiguration" + } + ], + "description": "Gets or sets the configuration.", + "nullable": true + }, + "Policy": { + "allOf": [ + { + "$ref": "#/components/schemas/UserPolicy" + } + ], + "description": "Gets or sets the policy.", + "nullable": true + }, + "PrimaryImageAspectRatio": { + "type": "number", + "description": "Gets or sets the primary image aspect ratio.", + "format": "double", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class UserDto." + }, + "UserItemDataDto": { + "type": "object", + "properties": { + "Rating": { + "type": "number", + "description": "Gets or sets the rating.", + "format": "double", + "nullable": true + }, + "PlayedPercentage": { + "type": "number", + "description": "Gets or sets the played percentage.", + "format": "double", + "nullable": true + }, + "UnplayedItemCount": { + "type": "integer", + "description": "Gets or sets the unplayed item count.", + "format": "int32", + "nullable": true + }, + "PlaybackPositionTicks": { + "type": "integer", + "description": "Gets or sets the playback position ticks.", + "format": "int64" + }, + "PlayCount": { + "type": "integer", + "description": "Gets or sets the play count.", + "format": "int32" + }, + "IsFavorite": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is favorite." + }, + "Likes": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Dto.UserItemDataDto is likes.", + "nullable": true + }, + "LastPlayedDate": { + "type": "string", + "description": "Gets or sets the last played date.", + "format": "date-time", + "nullable": true + }, + "Played": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Dto.UserItemDataDto is played." + }, + "Key": { + "type": "string", + "description": "Gets or sets the key." + }, + "ItemId": { + "type": "string", + "description": "Gets or sets the item identifier.", + "format": "uuid" + } + }, + "additionalProperties": false, + "description": "Class UserItemDataDto." + }, + "UserPolicy": { + "required": [ + "AuthenticationProviderId", + "PasswordResetProviderId" + ], + "type": "object", + "properties": { + "IsAdministrator": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is administrator." + }, + "IsHidden": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is hidden." + }, + "EnableCollectionManagement": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance can manage collections.", + "default": false + }, + "EnableSubtitleManagement": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance can manage subtitles.", + "default": false + }, + "EnableLyricManagement": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this user can manage lyrics.", + "default": false + }, + "IsDisabled": { + "type": "boolean", + "description": "Gets or sets a value indicating whether this instance is disabled." + }, + "MaxParentalRating": { + "type": "integer", + "description": "Gets or sets the max parental rating.", + "format": "int32", + "nullable": true + }, + "MaxParentalSubRating": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "BlockedTags": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "AllowedTags": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "EnableUserPreferenceAccess": { + "type": "boolean" + }, + "AccessSchedules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AccessSchedule" + }, + "nullable": true + }, + "BlockUnratedItems": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnratedItem" + }, + "nullable": true + }, + "EnableRemoteControlOfOtherUsers": { + "type": "boolean" + }, + "EnableSharedDeviceControl": { + "type": "boolean" + }, + "EnableRemoteAccess": { + "type": "boolean" + }, + "EnableLiveTvManagement": { + "type": "boolean" + }, + "EnableLiveTvAccess": { + "type": "boolean" + }, + "EnableMediaPlayback": { + "type": "boolean" + }, + "EnableAudioPlaybackTranscoding": { + "type": "boolean" + }, + "EnableVideoPlaybackTranscoding": { + "type": "boolean" + }, + "EnablePlaybackRemuxing": { + "type": "boolean" + }, + "ForceRemoteSourceTranscoding": { + "type": "boolean" + }, + "EnableContentDeletion": { + "type": "boolean" + }, + "EnableContentDeletionFromFolders": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "EnableContentDownloading": { + "type": "boolean" + }, + "EnableSyncTranscoding": { + "type": "boolean", + "description": "Gets or sets a value indicating whether [enable synchronize]." + }, + "EnableMediaConversion": { + "type": "boolean" + }, + "EnabledDevices": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "EnableAllDevices": { + "type": "boolean" + }, + "EnabledChannels": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "nullable": true + }, + "EnableAllChannels": { + "type": "boolean" + }, + "EnabledFolders": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "nullable": true + }, + "EnableAllFolders": { + "type": "boolean" + }, + "InvalidLoginAttemptCount": { + "type": "integer", + "format": "int32" + }, + "LoginAttemptsBeforeLockout": { + "type": "integer", + "format": "int32" + }, + "MaxActiveSessions": { + "type": "integer", + "format": "int32" + }, + "EnablePublicSharing": { + "type": "boolean" + }, + "BlockedMediaFolders": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "nullable": true + }, + "BlockedChannels": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "nullable": true + }, + "RemoteClientBitrateLimit": { + "type": "integer", + "format": "int32" + }, + "AuthenticationProviderId": { + "type": "string" + }, + "PasswordResetProviderId": { + "type": "string" + }, + "SyncPlayAccess": { + "enum": [ + "CreateAndJoinGroups", + "JoinGroups", + "None" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SyncPlayUserAccessType" + } + ], + "description": "Gets or sets a value indicating what SyncPlay features the user can access." + } + }, + "additionalProperties": false + }, + "UserUpdatedMessage": { + "type": "object", + "properties": { + "Data": { + "allOf": [ + { + "$ref": "#/components/schemas/UserDto" + } + ], + "description": "Class UserDto.", + "nullable": true + }, + "MessageId": { + "type": "string", + "description": "Gets or sets the message id.", + "format": "uuid" + }, + "MessageType": { + "enum": [ + "ForceKeepAlive", + "GeneralCommand", + "UserDataChanged", + "Sessions", + "Play", + "SyncPlayCommand", + "SyncPlayGroupUpdate", + "Playstate", + "RestartRequired", + "ServerShuttingDown", + "ServerRestarting", + "LibraryChanged", + "UserDeleted", + "UserUpdated", + "SeriesTimerCreated", + "TimerCreated", + "SeriesTimerCancelled", + "TimerCancelled", + "RefreshProgress", + "ScheduledTaskEnded", + "PackageInstallationCancelled", + "PackageInstallationFailed", + "PackageInstallationCompleted", + "PackageInstalling", + "PackageUninstalled", + "ActivityLogEntry", + "ScheduledTasksInfo", + "ActivityLogEntryStart", + "ActivityLogEntryStop", + "SessionsStart", + "SessionsStop", + "ScheduledTasksInfoStart", + "ScheduledTasksInfoStop", + "KeepAlive" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SessionMessageType" + } + ], + "description": "The different kinds of messages that are used in the WebSocket api.", + "default": "UserUpdated", + "readOnly": true + } + }, + "additionalProperties": false, + "description": "User updated message." + }, + "UtcTimeResponse": { + "type": "object", + "properties": { + "RequestReceptionTime": { + "type": "string", + "description": "Gets the UTC time when request has been received.", + "format": "date-time" + }, + "ResponseTransmissionTime": { + "type": "string", + "description": "Gets the UTC time when response has been sent.", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "Class UtcTimeResponse." + }, + "ValidatePathDto": { + "type": "object", + "properties": { + "ValidateWritable": { + "type": "boolean", + "description": "Gets or sets a value indicating whether validate if path is writable." + }, + "Path": { + "type": "string", + "description": "Gets or sets the path.", + "nullable": true + }, + "IsFile": { + "type": "boolean", + "description": "Gets or sets is path file.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Validate path object." + }, + "VersionInfo": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "Gets or sets the version." + }, + "VersionNumber": { + "type": "string", + "description": "Gets the version as a System.Version.", + "readOnly": true + }, + "changelog": { + "type": "string", + "description": "Gets or sets the changelog for this version.", + "nullable": true + }, + "targetAbi": { + "type": "string", + "description": "Gets or sets the ABI that this version was built against.", + "nullable": true + }, + "sourceUrl": { + "type": "string", + "description": "Gets or sets the source URL.", + "nullable": true + }, + "checksum": { + "type": "string", + "description": "Gets or sets a checksum for the binary.", + "nullable": true + }, + "timestamp": { + "type": "string", + "description": "Gets or sets a timestamp of when the binary was built.", + "nullable": true + }, + "repositoryName": { + "type": "string", + "description": "Gets or sets the repository name." + }, + "repositoryUrl": { + "type": "string", + "description": "Gets or sets the repository url." + } + }, + "additionalProperties": false, + "description": "Defines the MediaBrowser.Model.Updates.VersionInfo class." + }, + "Video3DFormat": { + "enum": [ + "HalfSideBySide", + "FullSideBySide", + "FullTopAndBottom", + "HalfTopAndBottom", + "MVC" + ], + "type": "string" + }, + "VideoRange": { + "enum": [ + "Unknown", + "SDR", + "HDR" + ], + "type": "string", + "description": "An enum representing video ranges." + }, + "VideoRangeType": { + "enum": [ + "Unknown", + "SDR", + "HDR10", + "HLG", + "DOVI", + "DOVIWithHDR10", + "DOVIWithHLG", + "DOVIWithSDR", + "DOVIWithEL", + "DOVIWithHDR10Plus", + "DOVIWithELHDR10Plus", + "DOVIInvalid", + "HDR10Plus" + ], + "type": "string", + "description": "An enum representing types of video ranges." + }, + "VideoType": { + "enum": [ + "VideoFile", + "Iso", + "Dvd", + "BluRay" + ], + "type": "string", + "description": "Enum VideoType." + }, + "VirtualFolderInfo": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Gets or sets the name.", + "nullable": true + }, + "Locations": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets or sets the locations.", + "nullable": true + }, + "CollectionType": { + "enum": [ + "movies", + "tvshows", + "music", + "musicvideos", + "homevideos", + "boxsets", + "books", + "mixed" + ], + "allOf": [ + { + "$ref": "#/components/schemas/CollectionTypeOptions" + } + ], + "description": "Gets or sets the type of the collection.", + "nullable": true + }, + "LibraryOptions": { + "allOf": [ + { + "$ref": "#/components/schemas/LibraryOptions" + } + ], + "nullable": true + }, + "ItemId": { + "type": "string", + "description": "Gets or sets the item identifier.", + "nullable": true + }, + "PrimaryImageItemId": { + "type": "string", + "description": "Gets or sets the primary image item identifier.", + "nullable": true + }, + "RefreshProgress": { + "type": "number", + "format": "double", + "nullable": true + }, + "RefreshStatus": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Used to hold information about a user's list of configured virtual folders." + }, + "WebSocketMessage": { + "type": "object", + "oneOf": [ + { + "$ref": "#/components/schemas/InboundWebSocketMessage" + }, + { + "$ref": "#/components/schemas/OutboundWebSocketMessage" + } + ], + "description": "Represents the possible websocket types" + }, + "XbmcMetadataOptions": { + "type": "object", + "properties": { + "UserId": { + "type": "string", + "nullable": true + }, + "ReleaseDateFormat": { + "type": "string" + }, + "SaveImagePathsInNfo": { + "type": "boolean" + }, + "EnablePathSubstitution": { + "type": "boolean" + }, + "EnableExtraThumbsDuplication": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "securitySchemes": { + "CustomAuthentication": { + "type": "apiKey", + "description": "API key header parameter", + "name": "Authorization", + "in": "header" + } + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index f9b07e9..f937814 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,84 @@ services: - octo-fiestarr: - image: ghcr.io/bransoned/octo-fiestarr - container_name: octo-fiestarr + redis: + image: redis:7-alpine + container_name: allstarr-redis + restart: unless-stopped + # Redis is only accessible internally - no external port exposure + expose: + - "6379" + command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 3 + networks: + - allstarr-network + + allstarr: + build: + context: . + dockerfile: Dockerfile + image: allstarr:local + container_name: allstarr restart: unless-stopped ports: - "5274:8080" + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - allstarr-network environment: - ASPNETCORE_ENVIRONMENT=Production - # Navidrome/Subsonic server URL + # Backend type: Subsonic or Jellyfin (default: Subsonic) + - Backend__Type=${BACKEND_TYPE:-Subsonic} + + # ===== REDIS CACHE ===== + - Redis__ConnectionString=redis:6379 + - Redis__Enabled=${REDIS_ENABLED:-true} + + # ===== SUBSONIC BACKEND ===== - Subsonic__Url=${SUBSONIC_URL:-http://localhost:4533} - # Explicit content filter: All, ExplicitOnly, CleanOnly (default: ExplicitOnly) - Subsonic__ExplicitFilter=${EXPLICIT_FILTER:-ExplicitOnly} - # Download mode: Track (only requested track), Album (full album when playing a track) - Subsonic__DownloadMode=${DOWNLOAD_MODE:-Track} - # Download path inside container + - Subsonic__MusicService=${MUSIC_SERVICE:-SquidWTF} + - Subsonic__StorageMode=${STORAGE_MODE:-Permanent} + - Subsonic__CacheDurationHours=${CACHE_DURATION_HOURS:-1} + - Subsonic__EnableExternalPlaylists=${ENABLE_EXTERNAL_PLAYLISTS:-true} + + # ===== JELLYFIN BACKEND ===== + - Jellyfin__Url=${JELLYFIN_URL:-http://localhost:8096} + - Jellyfin__ApiKey=${JELLYFIN_API_KEY:-} + - Jellyfin__UserId=${JELLYFIN_USER_ID:-} + - Jellyfin__LibraryId=${JELLYFIN_LIBRARY_ID:-} + - Jellyfin__ClientUsername=${JELLYFIN_CLIENT_USERNAME:-} + - Jellyfin__ExplicitFilter=${EXPLICIT_FILTER:-ExplicitOnly} + - Jellyfin__DownloadMode=${DOWNLOAD_MODE:-Track} + - Jellyfin__MusicService=${MUSIC_SERVICE:-SquidWTF} + - Jellyfin__StorageMode=${STORAGE_MODE:-Permanent} + - Jellyfin__CacheDurationHours=${CACHE_DURATION_HOURS:-1} + - Jellyfin__EnableExternalPlaylists=${ENABLE_EXTERNAL_PLAYLISTS:-true} + + # ===== SHARED ===== - Library__DownloadPath=/app/downloads - # SquidWTF preferred audio quality - SquidWTF__Quality=${SQUIDWTF_QUALITY:-FLAC} - # Set cache or permanent download of external songs - - Storage__Mode=${STORAGE_MODE:-Permanent} - # Set cache duration - - Cache__DurationHours=${CACHE_DURATION_HOURS:-1} + - Deezer__Arl=${DEEZER_ARL:-} + - Deezer__ArlFallback=${DEEZER_ARL_FALLBACK:-} + - Deezer__Quality=${DEEZER_QUALITY:-FLAC} + - Qobuz__UserAuthToken=${QOBUZ_USER_AUTH_TOKEN:-} + - Qobuz__UserId=${QOBUZ_USER_ID:-} + - Qobuz__Quality=${QOBUZ_QUALITY:-FLAC} volumes: - ${DOWNLOAD_PATH:-./downloads}:/app/downloads + +networks: + allstarr-network: + name: allstarr-network + driver: bridge diff --git a/my-changes.patch b/my-changes.patch new file mode 100644 index 0000000..21b6cb4 --- /dev/null +++ b/my-changes.patch @@ -0,0 +1,91356 @@ +diff --git a/.env.example b/.env.example +index ff114a1..876ba2f 100644 +--- a/.env.example ++++ b/.env.example +@@ -1,13 +1,36 @@ +-# Navidrome/Subsonic server URL ++# ===== BACKEND SELECTION ===== ++# Choose which media server backend to use: Subsonic or Jellyfin ++BACKEND_TYPE=Subsonic ++ ++# ===== REDIS CACHE ===== ++# Enable Redis caching for metadata and images (default: true) ++REDIS_ENABLED=true ++ ++# ===== SUBSONIC/NAVIDROME CONFIGURATION ===== ++# Server URL (required if using Subsonic backend) + SUBSONIC_URL=http://localhost:4533 + +-# Path where downloaded songs will be stored on the host (only applies if STORAGE_MODE=Permanent) +-DOWNLOAD_PATH=./downloads ++# ===== JELLYFIN CONFIGURATION ===== ++# Server URL (required if using Jellyfin backend) ++JELLYFIN_URL=http://localhost:8096 ++ ++# API key for authentication (get from Jellyfin Dashboard > API Keys) ++JELLYFIN_API_KEY= + ++# User ID (get from Jellyfin Dashboard > Users > click user > check URL) ++JELLYFIN_USER_ID= ++ ++# Music library ID (optional, auto-detected if not set) ++JELLYFIN_LIBRARY_ID= ++ ++# ===== MUSIC SOURCE SELECTION ===== + # Music service to use: SquidWTF, Deezer, or Qobuz (default: SquidWTF) + MUSIC_SERVICE=SquidWTF + +-# ===== SquidWTF CONFIGURATION ===== ++# Path where downloaded songs will be stored on the host (only applies if STORAGE_MODE=Permanent) ++DOWNLOAD_PATH=./downloads ++ ++# ===== SQUIDWTF CONFIGURATION ===== + # Different quality options for SquidWTF. Only FLAC supported right now + SQUIDWTF_QUALITY=FLAC + +@@ -61,14 +84,14 @@ EXPLICIT_FILTER=All + DOWNLOAD_MODE=Track + + # Storage mode (optional, default: Permanent) +-# - Permanent: Files are saved to the library permanently and registered in Navidrome ++# - Permanent: Files are saved to the library permanently and registered in the media server + # - Cache: Files are stored in /tmp and automatically cleaned up after CACHE_DURATION_HOURS +-# Not registered in Navidrome, ideal for streaming without library bloat ++# Not registered in media server, ideal for streaming without library bloat + # Note: On Linux/Docker, you can customize cache location by setting TMPDIR environment variable + STORAGE_MODE=Permanent + + # Cache duration in hours (optional, default: 1) + # Files older than this duration will be automatically deleted when STORAGE_MODE=Cache + # Based on last access time (updated each time the file is streamed) +-# Cache location: /tmp/octo-fiesta-cache (or $TMPDIR/octo-fiesta-cache if TMPDIR is set) ++# Cache location: /tmp/allstarr-cache (or $TMPDIR/allstarr-cache if TMPDIR is set) + CACHE_DURATION_HOURS=1 +diff --git a/.gitignore b/.gitignore +index 81b563b..53b7bb2 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -68,7 +68,16 @@ obj/ + # Autres fichiers temporaires + *.log + +-/.env +- +-# Downloaded music files +-octo-fiesta/downloads/ +\ No newline at end of file ++/.env ++ ++# Downloaded music files ++downloads/ ++!downloads/.gitkeep ++ ++# Docker volumes ++redis-data/ ++ ++# API keys and specs (ignore markdown docs, keep OpenAPI spec) ++apis/*.md ++apis/*.json ++!apis/jellyfin-openapi-stable.json +\ No newline at end of file +diff --git a/Dockerfile b/Dockerfile +index f49119c..e8b5697 100644 +--- a/Dockerfile ++++ b/Dockerfile +@@ -2,21 +2,24 @@ + FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build + WORKDIR /src + +-COPY octo-fiesta.sln . +-COPY octo-fiesta/octo-fiesta.csproj octo-fiesta/ +-COPY octo-fiesta.Tests/octo-fiesta.Tests.csproj octo-fiesta.Tests/ ++COPY allstarr.sln . ++COPY allstarr/allstarr.csproj allstarr/ ++COPY allstarr.Tests/allstarr.Tests.csproj allstarr.Tests/ + + RUN dotnet restore + +-COPY octo-fiesta/ octo-fiesta/ +-COPY octo-fiesta.Tests/ octo-fiesta.Tests/ ++COPY allstarr/ allstarr/ ++COPY allstarr.Tests/ allstarr.Tests/ + +-RUN dotnet publish octo-fiesta/octo-fiesta.csproj -c Release -o /app/publish ++RUN dotnet publish allstarr/allstarr.csproj -c Release -o /app/publish + + # Runtime stage + FROM mcr.microsoft.com/dotnet/aspnet:9.0 + WORKDIR /app + ++# Install curl for health checks ++RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* ++ + RUN mkdir -p /app/downloads + + COPY --from=build /app/publish . +@@ -24,4 +27,4 @@ COPY --from=build /app/publish . + EXPOSE 8080 + ENV ASPNETCORE_URLS=http://+:8080 + +-ENTRYPOINT ["dotnet", "octo-fiesta.dll"] ++ENTRYPOINT ["dotnet", "allstarr.dll"] +diff --git a/README.md b/README.md +index 93dce47..f85821b 100644 +--- a/README.md ++++ b/README.md +@@ -1,41 +1,107 @@ +-# Octo-Fiestarr ++# Allstarr + +-A Subsonic API proxy server that transparently integrates multiple music streaming providers as sources. When a song is not available in your local Navidrome library, it is automatically fetched from your configured provider, downloaded, and served to your Subsonic-compatible client. The downloaded song is then added to your library, making it available locally for future listens. ++A media server proxy that integrates music streaming providers with your local library. Works with **Jellyfin** and **Subsonic-compatible** servers (Navidrome). When a song isn't in your local library, it gets fetched from your configured provider, downloaded, and served to your client. The downloaded song then lives in your library for next time. + +-## Why "Octo-Fiestarr"? ++**THIS IS UNDER ACTIVE DEVELOPMENT** + +-This fork was created to focus on integrating the original concept of Octo-Fiesta with music providers that do not require API credentials, such as SquidWTF. This allows for seamless external music discovery without the need for any subscriptions. Thus, I saw it fitting to change the name of the fork to resemble other *arr projects. ++Please report all bugs as soon as possible, as the Jellyfin addition is entirely a test at this point ++ ++## Quick Start ++ ++```bash ++# 1. Configure environment ++cp .env.example .env ++nano .env # Edit with your settings ++ ++# 2. Start services ++docker-compose up -d --build ++ ++# 3. Check status ++docker-compose ps ++docker-compose logs -f ++``` ++ ++### Nginx Proxy Setup (Required) ++ ++This service only exposes ports internally. You **must** use nginx to proxy to it: ++ ++```nginx ++server { ++ listen 443 ssl http2; ++ server_name your-domain.com; ++ ++ ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; ++ ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; ++ ssl_protocols TLSv1.2 TLSv1.3; ++ ++ # Security headers ++ add_header Strict-Transport-Security "max-age=31536000" always; ++ add_header X-Content-Type-Options "nosniff" always; ++ ++ # Streaming settings ++ proxy_buffering off; ++ proxy_request_buffering off; ++ proxy_read_timeout 600s; ++ ++ location / { ++ proxy_pass http://allstarr:8080; ++ proxy_set_header Host $host; ++ proxy_set_header X-Real-IP $remote_addr; ++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ++ proxy_set_header X-Forwarded-Proto $scheme; ++ } ++} ++``` ++ ++**Security:** All authentication is forwarded to Jellyfin - this is as secure as Jellyfin itself. Always use HTTPS for public access. ++ ++## Why "Allstarr"? ++ ++This project brings together all the music streaming providers into one unified library - making them all stars in your collection. + + ## Features + +-- **Multi-Provider Architecture**: Pluggable music service system supporting multiple streaming providers (Deezer, Qobuz, and more to come) +-- **Transparent Proxy**: Acts as a middleware between Subsonic clients (like Aonsoku, Sublime Music, etc.) and your Navidrome server +-- **Seamless Integration**: Automatically searches and streams music from your configured provider when not available locally +-- **Automatic Downloads**: Songs are downloaded on-the-fly and cached for future use +-- **External Playlist Support**: Search, discover, and download playlists from Deezer, Qobuz, and SquidWTF with automatic M3U generation +-- **Hi-Res Audio Support**: SquidWTF provider supports up to 24-bit/192kHz FLAC quality +-- **Full Metadata Embedding**: Downloaded files include complete ID3 tags (title, artist, album, track number, year, genre, BPM, ISRC, etc.) and embedded cover art +-- **Organized Library**: Downloads are saved in a clean `Artist/Album/Track` folder structure +-- **Artist Deduplication**: Merges local and streaming provider artists to avoid duplicates in search results +-- **Album Enrichment**: Local albums are enriched with missing tracks from streaming providers +-- **Cover Art Proxy**: Serves cover art for external content transparently ++- **Dual Backend Support**: Works with Jellyfin and Subsonic-compatible servers (Navidrome, Airsonic, etc.) ++- **Multi-Provider Architecture**: Pluggable system for streaming providers (Deezer, Qobuz, SquidWTF) ++- **Transparent Proxy**: Sits between your music clients and media server ++- **Automatic Search**: Searches streaming providers when songs aren't local ++- **On-the-Fly Downloads**: Songs download and cache for future use ++- **External Playlist Support**: Search and download playlists from Deezer, Qobuz, and SquidWTF with M3U generation ++- **Hi-Res Audio**: SquidWTF supports up to 24-bit/192kHz FLAC ++- **Full Metadata**: Downloaded files include complete ID3 tags (title, artist, album, track number, year, genre, BPM, ISRC, etc.) and cover art ++- **Organized Library**: Downloads save in `Artist/Album/Track` folder structure ++- **Artist Deduplication**: Merges local and streaming artists to avoid duplicates ++- **Album Enrichment**: Adds missing tracks to local albums from streaming providers ++- **Cover Art Proxy**: Serves cover art for external content ++ ++## Supported Backends ++ ++### Jellyfin ++[Jellyfin](https://jellyfin.org/) is a free and open-source media server. Allstarr connects via the Jellyfin API using your Jellyfin user login. (I plan to move this to api key if possible) ++ ++**Compatible Jellyfin clients:** ++ ++- [Feishin](https://github.com/jeffvli/feishin) (Mac/Windows/Linux) ++- [Musiver](https://music.aqzscn.cn/en/) (Android/IOS/Windows/Android) + +-## Compatible Clients ++_Working on getting more currently_ + +-### PC ++### Subsonic/Navidrome ++[Navidrome](https://www.navidrome.org/) and other Subsonic-compatible servers are supported via the Subsonic API. + ++**Compatible Subsonic clients:** ++ ++#### PC + - [Aonsoku](https://github.com/victoralvesf/aonsoku) + - [Feishin](https://github.com/jeffvli/feishin) + - [Subplayer](https://github.com/peguerosdc/subplayer) + - [Aurial](https://github.com/shrimpza/aurial) + +-### Android +- ++#### Android + - [Tempus](https://github.com/eddyizm/tempus) + - [Substreamer](https://substreamerapp.com/) + +-### iOS +- ++#### iOS + - [Narjo](https://www.reddit.com/r/NarjoApp/) + - [Arpeggi](https://www.reddit.com/r/arpeggiApp/) + +@@ -43,7 +109,7 @@ This fork was created to focus on integrating the original concept of Octo-Fiest + + ### Incompatible Clients + +-These clients are **not compatible** with octo-fiesta due to architectural limitations: ++These clients are **not compatible** with Allstarr due to architectural limitations: + + - [Symfonium](https://symfonium.app/) - Uses offline-first architecture and never queries the server for searches, making streaming provider integration impossible. [See details](https://support.symfonium.app/t/suggestions-on-search-function/1121/) + +@@ -57,15 +123,17 @@ Choose your preferred provider via the `MUSIC_SERVICE` environment variable. Add + + ## Requirements + +-- A running Subsonic-compatible server (developed and tested with [Navidrome](https://www.navidrome.org/)) ++- A running media server: ++ - **Jellyfin**: Any recent version with API access enabled ++ - **Subsonic**: Navidrome or other Subsonic-compatible server + - Credentials for at least one music provider (IF NOT USING SQUIDWTF): + - **Deezer**: ARL token from browser cookies +- - **Qobuz**: User ID + User Auth Token from browser localStorage ([see Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token))) ++ - **Qobuz**: User ID + User Auth Token from browser localStorage ([see Wiki guide](https://github.com/V1ck3s/allstarr/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token))) + - Docker and Docker Compose (recommended) **or** [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) for manual installation + + ## Quick Start (Docker) + +-The easiest way to run Octo-Fiestarr is with Docker Compose. ++The easiest way to run Allstarr is with Docker Compose. + + 1. **Create your environment file** + ```bash +@@ -73,82 +141,47 @@ The easiest way to run Octo-Fiestarr is with Docker Compose. + ``` + + 2. **Edit the `.env` file** with your configuration: ++ ++ **For Jellyfin backend:** ++ ```bash ++ # Backend selection ++ BACKEND_TYPE=Jellyfin ++ ++ # Jellyfin server URL ++ JELLYFIN_URL=http://localhost:8096 ++ ++ # API key (get from Jellyfin Dashboard > API Keys) ++ JELLYFIN_API_KEY=your-api-key-here ++ ++ # User ID (from Jellyfin Dashboard > Users > click user > check URL) ++ JELLYFIN_USER_ID=your-user-id-here ++ ++ # Music library ID (optional, auto-detected if not set) ++ JELLYFIN_LIBRARY_ID= ++ ``` ++ ++ **For Subsonic/Navidrome backend:** + ```bash +- # Navidrome/Subsonic server URL +- SUBSONIC_URL=http://localhost:4533 +- +- # Path where downloaded songs will be stored on the host (only applies if STORAGE_MODE=Permanent) +- DOWNLOAD_PATH=./downloads +- +- # Music service to use: SquidWTF, Deezer, or Qobuz (default: SquidWTF) +- MUSIC_SERVICE=SquidWTF +- +- # ===== SquidWTF CONFIGURATION ===== +- # Different quality options for SquidWTF. Only FLAC supported right now +- SQUIDWTF_QUALITY=FLAC +- +- # ===== DEEZER CONFIGURATION ===== +- # Deezer ARL token (required if using Deezer) +- # See README.md for instructions on how to get this token +- DEEZER_ARL=your-deezer-arl-token +- +- # Fallback ARL token (optional) +- DEEZER_ARL_FALLBACK= +- +- # Preferred audio quality: FLAC, MP3_320, MP3_128 (optional) +- # If not specified, the highest available quality for your account will be used +- DEEZER_QUALITY= +- +- # ===== QOBUZ CONFIGURATION ===== +- # Qobuz user authentication token (required if using Qobuz) +- # Get this from your browser after logging into play.qobuz.com +- # See README.md for detailed instructions +- QOBUZ_USER_AUTH_TOKEN= +- +- # Qobuz user ID (required if using Qobuz) +- # Get this from your browser after logging into play.qobuz.com +- QOBUZ_USER_ID= +- +- # Preferred audio quality: FLAC, FLAC_24_HIGH, FLAC_24_LOW, FLAC_16, MP3_320 (optional) +- # If not specified, the highest available quality will be used +- QOBUZ_QUALITY= +- +- # ===== GENERAL SETTINGS ===== +- # External playlists support (optional, default: true) +- # When enabled, allows searching and downloading playlists from Deezer/Qobuz +- # Starring a playlist triggers automatic download of all tracks and creates an M3U file +- ENABLE_EXTERNAL_PLAYLISTS=true +- +- # Playlists directory name (optional, default: playlists) +- # M3U playlist files will be created in {DOWNLOAD_PATH}/{PLAYLISTS_DIRECTORY}/ +- PLAYLISTS_DIRECTORY=playlists +- +- # Explicit content filter (optional, default: All) +- # - All: Show all tracks (no filtering) +- # - ExplicitOnly: Exclude clean/edited versions, keep original explicit content +- # - CleanOnly: Only show clean content (naturally clean or edited versions) +- # Note: This only works with Deezer, Qobuz doesn't expose explicit content flags +- EXPLICIT_FILTER=All +- +- # Download mode (optional, default: Track) +- # - Track: Download only the played track +- # - Album: When playing a track, download the entire album in background +- # The played track is downloaded first, remaining tracks are queued +- DOWNLOAD_MODE=Track +- +- # Storage mode (optional, default: Permanent) +- # - Permanent: Files are saved to the library permanently and registered in Navidrome +- # - Cache: Files are stored in /tmp and automatically cleaned up after CACHE_DURATION_HOURS +- # Not registered in Navidrome, ideal for streaming without library bloat +- # Note: On Linux/Docker, you can customize cache location by setting TMPDIR environment variable +- STORAGE_MODE=Permanent +- +- # Cache duration in hours (optional, default: 1) +- # Files older than this duration will be automatically deleted when STORAGE_MODE=Cache +- # Based on last access time (updated each time the file is streamed) +- # Cache location: /tmp/octo-fiesta-cache (or $TMPDIR/octo-fiesta-cache if TMPDIR is set) +- CACHE_DURATION_HOURS=1 +- ``` ++ # Backend selection ++ BACKEND_TYPE=Subsonic ++ ++ # Navidrome/Subsonic server URL ++ SUBSONIC_URL=http://localhost:4533 ++ ``` ++ ++ **Common settings (both backends):** ++ ```bash ++ # Path where downloaded songs will be stored ++ DOWNLOAD_PATH=./downloads ++ ++ # Music service to use: SquidWTF, Deezer, or Qobuz ++ MUSIC_SERVICE=SquidWTF ++ ++ # Storage mode: Permanent or Cache ++ STORAGE_MODE=Permanent ++ ``` ++ ++ See the full `.env.example` for all available options including Deezer/Qobuz credentials. + + 3. **Start the container** + ```bash +@@ -157,21 +190,47 @@ The easiest way to run Octo-Fiestarr is with Docker Compose. + + The proxy will be available at `http://localhost:5274`. + +-4. **Configure your Subsonic client** ++4. **Configure your client** + +- Point your Subsonic client to `http://localhost:5274` instead of your Navidrome server directly. ++ Point your music client to `http://localhost:5274` instead of your media server directly. + +-> **Tip**: Make sure the `DOWNLOAD_PATH` points to a directory that Navidrome can scan, so downloaded songs appear in your library. ++> **Tip**: Make sure the `DOWNLOAD_PATH` points to a directory that your media server can scan, so downloaded songs appear in your library. + + ## Configuration + +-### General Settings ++### Backend Selection ++ ++| Setting | Description | ++|---------|-------------| ++| `Backend:Type` | Backend type: `Subsonic` or `Jellyfin` (default: `Subsonic`) | ++ ++### Jellyfin Settings ++ ++| Setting | Description | ++|---------|-------------| ++| `Jellyfin:Url` | URL of your Jellyfin server | ++| `Jellyfin:ApiKey` | API key (get from Jellyfin Dashboard > API Keys) | ++| `Jellyfin:UserId` | User ID for library access | ++| `Jellyfin:LibraryId` | Music library ID (optional, auto-detected) | ++| `Jellyfin:MusicService` | Music provider: `SquidWTF`, `Deezer`, or `Qobuz` | ++ ++### Subsonic Settings + + | Setting | Description | + |---------|-------------| + | `Subsonic:Url` | URL of your Navidrome/Subsonic server | +-| `Subsonic:MusicService` | Music provider to use: `SquidWTF`, `Deezer`, or `Qobuz` (default: `SquidWTF`) | ++| `Subsonic:MusicService` | Music provider: `SquidWTF`, `Deezer`, or `Qobuz` (default: `SquidWTF`) | ++ ++### Shared Settings ++ ++| Setting | Description | ++|---------|-------------| + | `Library:DownloadPath` | Directory where downloaded songs are stored | ++| `*:ExplicitFilter` | Content filter: `All`, `ExplicitOnly`, or `CleanOnly` | ++| `*:DownloadMode` | Download mode: `Track` or `Album` | ++| `*:StorageMode` | Storage mode: `Permanent` or `Cache` | ++| `*:CacheDurationHours` | Cache expiration time in hours | ++| `*:EnableExternalPlaylists` | Enable external playlist support | + + ### SquidWTF Settings + +@@ -191,13 +250,13 @@ The easiest way to run Octo-Fiestarr is with Docker Compose. + + | Setting | Description | + |---------|-------------| +-| `Qobuz:UserAuthToken` | Your Qobuz User Auth Token (required if using Qobuz) - [How to get it](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token)) | ++| `Qobuz:UserAuthToken` | Your Qobuz User Auth Token (required if using Qobuz) - [How to get it](https://github.com/V1ck3s/allstarr/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token)) | + | `Qobuz:UserId` | Your Qobuz User ID (required if using Qobuz) | + | `Qobuz:Quality` | Preferred audio quality: `FLAC`, `FLAC_24_HIGH`, `FLAC_24_LOW`, `FLAC_16`, `MP3_320`. If not specified, the highest available quality will be used | + + ### External Playlists + +-Octo-Fiesta supports discovering and downloading playlists from your streaming providers (SquidWTF, Deezer, and Qobuz). ++Allstarr supports discovering and downloading playlists from your streaming providers (SquidWTF, Deezer, and Qobuz). + + | Setting | Description | + |---------|-------------| +@@ -206,7 +265,7 @@ Octo-Fiesta supports discovering and downloading playlists from your streaming p + + **How it works:** + 1. Search for playlists from an external provider using the global search in your Subsonic client +-2. When you "star" (favorite) a playlist, Octo-Fiesta automatically downloads all tracks ++2. When you "star" (favorite) a playlist, Allstarr automatically downloads all tracks + 3. An M3U playlist file is created in `{DownloadPath}/playlists/` with relative paths to downloaded tracks + 4. Individual tracks are added to the M3U as they are played or downloaded + +@@ -222,11 +281,11 @@ Subsonic__EnableExternalPlaylists=false + + #### Deezer ARL Token + +-See the [Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Deezer-Credentials-(ARL-Token)) for detailed instructions on obtaining your Deezer ARL token. ++See the [Wiki guide](https://github.com/V1ck3s/allstarr/wiki/Getting-Deezer-Credentials-(ARL-Token)) for detailed instructions on obtaining your Deezer ARL token. + + #### Qobuz Credentials + +-See the [Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token)) for detailed instructions on obtaining your Qobuz User ID and User Auth Token. ++See the [Wiki guide](https://github.com/V1ck3s/allstarr/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token)) for detailed instructions on obtaining your Qobuz User ID and User Auth Token. + + ## Limitations + +@@ -237,29 +296,39 @@ See the [Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Cr + ## Architecture + + ``` +-┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +-│ Subsonic │────▶│ Octo-Fiesta │────▶│ Navidrome │ +-│ Client │◀────│ (Proxy) │◀────│ Server │ +-│ (Aonsoku) │ │ │ │ │ +-└─────────────────┘ └────────┬─────────┘ └─────────────────┘ +- │ +- ▼ ++ ┌─────────────────┐ ++ ┌───▶│ Jellyfin │ ++┌─────────────────┐ ┌──────────────────┐ │ │ Server │ ++│ Music Client │────▶│ Allstarr │───┤ └─────────────────┘ ++│ (Aonsoku, │◀────│ (Proxy) │◀──┤ ++│ Finamp, etc.) │ │ │ │ ┌─────────────────┐ ++└─────────────────┘ └────────┬─────────┘ └───▶│ Navidrome │ ++ │ │ (Subsonic) │ ++ ▼ └─────────────────┘ + ┌─────────────────┐ + │ Music Providers │ ++ │ - SquidWTF │ + │ - Deezer │ + │ - Qobuz │ +- │ - (more...) │ + └─────────────────┘ + ``` + ++The proxy intercepts requests from your music client and: ++1. Forwards library requests to your configured backend (Jellyfin or Subsonic) ++2. Merges results with content from your music provider ++3. Downloads and caches external tracks on-demand ++4. Serves audio streams transparently ++ ++**Note**: Only the controller matching your configured `BACKEND_TYPE` is registered at runtime, preventing route conflicts and ensuring clean API separation. ++ + ## Manual Installation + +-If you prefer to run Octo-Fiesta without Docker: ++If you prefer to run Allstarr without Docker: + + 1. **Clone the repository** + ```bash +- git clone https://github.com/your-username/octo-fiesta.git +- cd octo-fiesta ++ git clone https://github.com/your-username/allstarr.git ++ cd allstarr + ``` + + 2. **Restore dependencies** +@@ -269,55 +338,63 @@ If you prefer to run Octo-Fiesta without Docker: + + 3. **Configure the application** + +- Edit `octo-fiesta/appsettings.json`: ++ Edit `allstarr/appsettings.json`: ++ ++ **For Jellyfin:** + ```json +-{ +- "Subsonic": { +- "Url": "https://navidrome.local.bransonb.com", +- "MusicService": "SquidWTF", +- "ExplicitFilter": "All", +- "DownloadMode": "Track", +- "StorageMode": "Permanent", +- "CacheDurationHours": 1 +- }, +- "Library": { +- "DownloadPath": "./downloads" +- }, +- "Qobuz": { +- "UserAuthToken": "your-qobuz-token", +- "UserId": "your-qobuz-user-id", +- "Quality": "FLAC" +- }, +- "Deezer": { +- "Arl": "your-deezer-arl-token", +- "ArlFallback": "", +- "Quality": "FLAC" +- }, +- "SquidWTF": { +- "Quality": "FLAC" +- } +-} +-``` ++ { ++ "Backend": { ++ "Type": "Jellyfin" ++ }, ++ "Jellyfin": { ++ "Url": "http://localhost:8096", ++ "ApiKey": "your-api-key", ++ "UserId": "your-user-id", ++ "MusicService": "SquidWTF" ++ }, ++ "Library": { ++ "DownloadPath": "./downloads" ++ } ++ } ++ ``` ++ ++ **For Subsonic/Navidrome:** ++ ```json ++ { ++ "Backend": { ++ "Type": "Subsonic" ++ }, ++ "Subsonic": { ++ "Url": "http://localhost:4533", ++ "MusicService": "SquidWTF" ++ }, ++ "Library": { ++ "DownloadPath": "./downloads" ++ } ++ } ++ ``` + + 4. **Run the server** + ```bash +- cd octo-fiesta ++ cd allstarr + dotnet run + ``` + + The proxy will start on `http://localhost:5274` by default. + +-5. **Configure your Subsonic client** ++5. **Configure your client** + +- Point your Subsonic client to `http://localhost:5274` instead of your Navidrome server directly. ++ Point your music client to `http://localhost:5274` instead of your media server directly. + + ## API Endpoints + +-The proxy implements the Subsonic API and adds transparent streaming provider integration to: ++### Subsonic Backend ++ ++The proxy implements the Subsonic API and adds transparent streaming provider integration: + + | Endpoint | Description | + |----------|-------------| +-| `GET /rest/search3` | Merged search results from Navidrome + streaming provider (including playlists) | ++| `GET /rest/search3` | Merged search results from Navidrome + streaming provider | + | `GET /rest/stream` | Streams audio, downloading from provider if needed | + | `GET /rest/getSong` | Returns song details (local or from provider) | + | `GET /rest/getAlbum` | Returns album with tracks from both sources | +@@ -327,6 +404,20 @@ The proxy implements the Subsonic API and adds transparent streaming provider in + + All other Subsonic API endpoints are passed through to Navidrome unchanged. + ++### Jellyfin Backend ++ ++The proxy implements a subset of the Jellyfin API: ++ ++| Endpoint | Description | ++|----------|-------------| ++| `GET /Items` | Search and browse library items | ++| `GET /Artists` | Browse artists with streaming provider results | ++| `GET /Audio/{id}/stream` | Stream audio, downloading from provider if needed | ++| `GET /Items/{id}/Images/{type}` | Proxy cover art for external content | ++| `POST /UserFavoriteItems/{id}` | Favorite items; triggers playlist download | ++ ++All other Jellyfin API endpoints are passed through unchanged. ++ + ## External ID Format + + External (streaming provider) content uses typed IDs: +@@ -388,9 +479,10 @@ dotnet test + ### Project Structure + + ``` +-octo-fiesta/ ++allstarr/ + ├── Controllers/ +-│ └── SubsonicController.cs # Main API controller ++│ ├── JellyfinController.cs # Jellyfin API controller (registered when Backend:Type=Jellyfin) ++│ └── SubsonicController.cs # Subsonic API controller (registered when Backend:Type=Subsonic) + ├── Middleware/ + │ └── GlobalExceptionHandler.cs # Global error handling + ├── Models/ +@@ -444,7 +536,7 @@ octo-fiesta/ + ├── Program.cs # Application entry point + └── appsettings.json # Configuration + +-octo-fiesta.Tests/ ++allstarr.Tests/ + ├── DeezerDownloadServiceTests.cs # Deezer download tests + ├── DeezerMetadataServiceTests.cs # Deezer metadata tests + ├── QobuzDownloadServiceTests.cs # Qobuz download tests (127 tests) +diff --git a/allstarr.Tests/DeezerDownloadServiceTests.cs b/allstarr.Tests/DeezerDownloadServiceTests.cs +new file mode 100644 +index 0000000..f0fa1a2 +--- /dev/null ++++ b/allstarr.Tests/DeezerDownloadServiceTests.cs +@@ -0,0 +1,476 @@ ++using allstarr.Services; ++using allstarr.Services.Deezer; ++using allstarr.Services.Local; ++using allstarr.Services.Common; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using Microsoft.Extensions.Configuration; ++using Microsoft.Extensions.Logging; ++using Microsoft.Extensions.Options; ++using Moq; ++using Moq.Protected; ++using System.Net; ++using System.Text.Json; ++ ++namespace allstarr.Tests; ++ ++public class DeezerDownloadServiceTests : IDisposable ++{ ++ private readonly Mock _httpClientFactoryMock; ++ private readonly Mock _httpMessageHandlerMock; ++ private readonly Mock _localLibraryServiceMock; ++ private readonly Mock _metadataServiceMock; ++ private readonly Mock> _loggerMock; ++ private readonly IConfiguration _configuration; ++ private readonly string _testDownloadPath; ++ ++ public DeezerDownloadServiceTests() ++ { ++ _testDownloadPath = Path.Combine(Path.GetTempPath(), "allstarr-download-tests-" + Guid.NewGuid()); ++ Directory.CreateDirectory(_testDownloadPath); ++ ++ _httpMessageHandlerMock = new Mock(); ++ var httpClient = new HttpClient(_httpMessageHandlerMock.Object); ++ ++ _httpClientFactoryMock = new Mock(); ++ _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); ++ ++ _localLibraryServiceMock = new Mock(); ++ _metadataServiceMock = new Mock(); ++ _loggerMock = new Mock>(); ++ ++ _configuration = new ConfigurationBuilder() ++ .AddInMemoryCollection(new Dictionary ++ { ++ ["Library:DownloadPath"] = _testDownloadPath, ++ ["Deezer:Arl"] = null, ++ ["Deezer:ArlFallback"] = null ++ }) ++ .Build(); ++ } ++ ++ public void Dispose() ++ { ++ if (Directory.Exists(_testDownloadPath)) ++ { ++ Directory.Delete(_testDownloadPath, true); ++ } ++ } ++ ++ private DeezerDownloadService CreateService(string? arl = null, DownloadMode downloadMode = DownloadMode.Track) ++ { ++ var config = new ConfigurationBuilder() ++ .AddInMemoryCollection(new Dictionary ++ { ++ ["Library:DownloadPath"] = _testDownloadPath, ++ ["Deezer:Arl"] = arl, ++ ["Deezer:ArlFallback"] = null ++ }) ++ .Build(); ++ ++ var subsonicSettings = Options.Create(new SubsonicSettings ++ { ++ DownloadMode = downloadMode ++ }); ++ ++ var deezerSettings = Options.Create(new DeezerSettings ++ { ++ Arl = arl, ++ ArlFallback = null, ++ Quality = null ++ }); ++ ++ var serviceProviderMock = new Mock(); ++ serviceProviderMock.Setup(sp => sp.GetService(typeof(allstarr.Services.Subsonic.PlaylistSyncService))) ++ .Returns(null); ++ ++ return new DeezerDownloadService( ++ _httpClientFactoryMock.Object, ++ config, ++ _localLibraryServiceMock.Object, ++ _metadataServiceMock.Object, ++ subsonicSettings, ++ deezerSettings, ++ serviceProviderMock.Object, ++ _loggerMock.Object); ++ } ++ ++ [Fact] ++ public async Task IsAvailableAsync_WithoutArl_ReturnsFalse() ++ { ++ // Arrange ++ var service = CreateService(arl: null); ++ ++ // Act ++ var result = await service.IsAvailableAsync(); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ [Fact] ++ public async Task IsAvailableAsync_WithEmptyArl_ReturnsFalse() ++ { ++ // Arrange ++ var service = CreateService(arl: ""); ++ ++ // Act ++ var result = await service.IsAvailableAsync(); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ [Fact] ++ public async Task DownloadSongAsync_WithUnsupportedProvider_ThrowsNotSupportedException() ++ { ++ // Arrange ++ var service = CreateService(arl: "test-arl"); ++ ++ // Act & Assert ++ await Assert.ThrowsAsync(() => ++ service.DownloadSongAsync("spotify", "123456")); ++ } ++ ++ [Fact] ++ public async Task DownloadSongAsync_WhenAlreadyDownloaded_ReturnsExistingPath() ++ { ++ // Arrange ++ var existingPath = Path.Combine(_testDownloadPath, "existing-song.mp3"); ++ await File.WriteAllTextAsync(existingPath, "fake audio content"); ++ ++ _localLibraryServiceMock ++ .Setup(s => s.GetLocalPathForExternalSongAsync("deezer", "123456")) ++ .ReturnsAsync(existingPath); ++ ++ var service = CreateService(arl: "test-arl"); ++ ++ // Act ++ var result = await service.DownloadSongAsync("deezer", "123456"); ++ ++ // Assert ++ Assert.Equal(existingPath, result); ++ } ++ ++ [Fact] ++ public void GetDownloadStatus_WithUnknownSongId_ReturnsNull() ++ { ++ // Arrange ++ var service = CreateService(arl: "test-arl"); ++ ++ // Act ++ var result = service.GetDownloadStatus("unknown-id"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ [Fact] ++ public async Task DownloadSongAsync_WhenSongNotFound_ThrowsException() ++ { ++ // Arrange ++ _localLibraryServiceMock ++ .Setup(s => s.GetLocalPathForExternalSongAsync("deezer", "999999")) ++ .ReturnsAsync((string?)null); ++ ++ _metadataServiceMock ++ .Setup(s => s.GetSongAsync("deezer", "999999")) ++ .ReturnsAsync((Song?)null); ++ ++ var service = CreateService(arl: "test-arl"); ++ ++ // Act & Assert ++ var exception = await Assert.ThrowsAsync(() => ++ service.DownloadSongAsync("deezer", "999999")); ++ ++ Assert.Equal("Song not found", exception.Message); ++ } ++ ++ [Fact] ++ public void DownloadRemainingAlbumTracksInBackground_WithUnsupportedProvider_DoesNotThrow() ++ { ++ // Arrange ++ var service = CreateService(arl: "test-arl", downloadMode: DownloadMode.Album); ++ ++ // Act & Assert - Should not throw, just log warning ++ service.DownloadRemainingAlbumTracksInBackground("spotify", "123456", "789"); ++ } ++ ++ [Fact] ++ public void DownloadRemainingAlbumTracksInBackground_WithDeezerProvider_StartsBackgroundTask() ++ { ++ // Arrange ++ _metadataServiceMock ++ .Setup(s => s.GetAlbumAsync("deezer", "123456")) ++ .ReturnsAsync(new Album ++ { ++ Id = "ext-deezer-album-123456", ++ Title = "Test Album", ++ Songs = new List ++ { ++ new Song { ExternalId = "111", Title = "Track 1" }, ++ new Song { ExternalId = "222", Title = "Track 2" } ++ } ++ }); ++ ++ var service = CreateService(arl: "test-arl", downloadMode: DownloadMode.Album); ++ ++ // Act - Should not throw (fire-and-forget) ++ service.DownloadRemainingAlbumTracksInBackground("deezer", "123456", "111"); ++ ++ // Assert - Just verify it doesn't throw, actual download is async ++ Assert.True(true); ++ } ++} ++ ++/// ++/// Unit tests for the PathHelper class that handles file organization logic. ++/// ++public class PathHelperTests : IDisposable ++{ ++ private readonly string _testPath; ++ ++ public PathHelperTests() ++ { ++ _testPath = Path.Combine(Path.GetTempPath(), "allstarr-pathhelper-tests-" + Guid.NewGuid()); ++ Directory.CreateDirectory(_testPath); ++ } ++ ++ public void Dispose() ++ { ++ if (Directory.Exists(_testPath)) ++ { ++ Directory.Delete(_testPath, true); ++ } ++ } ++ ++ #region SanitizeFileName Tests ++ ++ [Fact] ++ public void SanitizeFileName_WithValidName_ReturnsUnchanged() ++ { ++ // Arrange & Act ++ var result = PathHelper.SanitizeFileName("My Song Title"); ++ ++ // Assert ++ Assert.Equal("My Song Title", result); ++ } ++ ++ [Fact] ++ public void SanitizeFileName_WithInvalidChars_ReplacesWithUnderscore() ++ { ++ // Arrange - Use forward slash which is invalid on all platforms ++ var result = PathHelper.SanitizeFileName("Song/With/Invalid"); ++ ++ // Assert - Check that forward slashes were replaced with underscores ++ Assert.Equal("Song_With_Invalid", result); ++ } ++ ++ [Fact] ++ public void SanitizeFileName_WithNullOrEmpty_ReturnsUnknown() ++ { ++ // Arrange & Act ++ var resultNull = PathHelper.SanitizeFileName(null!); ++ var resultEmpty = PathHelper.SanitizeFileName(""); ++ var resultWhitespace = PathHelper.SanitizeFileName(" "); ++ ++ // Assert ++ Assert.Equal("Unknown", resultNull); ++ Assert.Equal("Unknown", resultEmpty); ++ Assert.Equal("Unknown", resultWhitespace); ++ } ++ ++ [Fact] ++ public void SanitizeFileName_WithLongName_TruncatesTo100Chars() ++ { ++ // Arrange ++ var longName = new string('A', 150); ++ ++ // Act ++ var result = PathHelper.SanitizeFileName(longName); ++ ++ // Assert ++ Assert.Equal(100, result.Length); ++ } ++ ++ #endregion ++ ++ #region SanitizeFolderName Tests ++ ++ [Fact] ++ public void SanitizeFolderName_WithValidName_ReturnsUnchanged() ++ { ++ // Arrange & Act ++ var result = PathHelper.SanitizeFolderName("Artist Name"); ++ ++ // Assert ++ Assert.Equal("Artist Name", result); ++ } ++ ++ [Fact] ++ public void SanitizeFolderName_WithNullOrEmpty_ReturnsUnknown() ++ { ++ // Arrange & Act ++ var resultNull = PathHelper.SanitizeFolderName(null!); ++ var resultEmpty = PathHelper.SanitizeFolderName(""); ++ var resultWhitespace = PathHelper.SanitizeFolderName(" "); ++ ++ // Assert ++ Assert.Equal("Unknown", resultNull); ++ Assert.Equal("Unknown", resultEmpty); ++ Assert.Equal("Unknown", resultWhitespace); ++ } ++ ++ [Fact] ++ public void SanitizeFolderName_WithTrailingDots_RemovesDots() ++ { ++ // Arrange & Act ++ var result = PathHelper.SanitizeFolderName("Artist Name..."); ++ ++ // Assert ++ Assert.Equal("Artist Name", result); ++ } ++ ++ [Fact] ++ public void SanitizeFolderName_WithInvalidChars_ReplacesWithUnderscore() ++ { ++ // Arrange - Use forward slash which is invalid on all platforms ++ var result = PathHelper.SanitizeFolderName("Artist/With/Invalid"); ++ ++ // Assert - Check that forward slashes were replaced with underscores ++ Assert.Equal("Artist_With_Invalid", result); ++ } ++ ++ #endregion ++ ++ #region BuildTrackPath Tests ++ ++ [Fact] ++ public void BuildTrackPath_WithAllParameters_CreatesCorrectStructure() ++ { ++ // Arrange ++ var downloadPath = "/downloads"; ++ var artist = "Test Artist"; ++ var album = "Test Album"; ++ var title = "Test Song"; ++ var trackNumber = 5; ++ var extension = ".mp3"; ++ ++ // Act ++ var result = PathHelper.BuildTrackPath(downloadPath, artist, album, title, trackNumber, extension); ++ ++ // Assert ++ Assert.Contains("Test Artist", result); ++ Assert.Contains("Test Album", result); ++ Assert.Contains("05 - Test Song.mp3", result); ++ } ++ ++ [Fact] ++ public void BuildTrackPath_WithoutTrackNumber_OmitsTrackPrefix() ++ { ++ // Arrange ++ var downloadPath = "/downloads"; ++ var artist = "Test Artist"; ++ var album = "Test Album"; ++ var title = "Test Song"; ++ var extension = ".mp3"; ++ ++ // Act ++ var result = PathHelper.BuildTrackPath(downloadPath, artist, album, title, null, extension); ++ ++ // Assert ++ Assert.Contains("Test Song.mp3", result); ++ Assert.DoesNotContain(" - Test Song", result.Split(Path.DirectorySeparatorChar).Last()); ++ } ++ ++ [Fact] ++ public void BuildTrackPath_WithSingleDigitTrack_PadsWithZero() ++ { ++ // Arrange & Act ++ var result = PathHelper.BuildTrackPath("/downloads", "Artist", "Album", "Song", 3, ".mp3"); ++ ++ // Assert ++ Assert.Contains("03 - Song.mp3", result); ++ } ++ ++ [Fact] ++ public void BuildTrackPath_WithFlacExtension_UsesFlacExtension() ++ { ++ // Arrange & Act ++ var result = PathHelper.BuildTrackPath("/downloads", "Artist", "Album", "Song", 1, ".flac"); ++ ++ // Assert ++ Assert.EndsWith(".flac", result); ++ } ++ ++ [Fact] ++ public void BuildTrackPath_CreatesArtistAlbumHierarchy() ++ { ++ // Arrange & Act ++ var result = PathHelper.BuildTrackPath("/downloads", "My Artist", "My Album", "My Song", 1, ".mp3"); ++ ++ // Assert ++ // Verify the structure is: downloadPath/Artist/Album/track.mp3 ++ var parts = result.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); ++ Assert.Contains("My Artist", parts); ++ Assert.Contains("My Album", parts); ++ ++ // Artist should come before Album in the path ++ var artistIndex = Array.IndexOf(parts, "My Artist"); ++ var albumIndex = Array.IndexOf(parts, "My Album"); ++ Assert.True(artistIndex < albumIndex, "Artist folder should be parent of Album folder"); ++ } ++ ++ #endregion ++ ++ #region ResolveUniquePath Tests ++ ++ [Fact] ++ public void ResolveUniquePath_WhenFileDoesNotExist_ReturnsSamePath() ++ { ++ // Arrange ++ var path = Path.Combine(_testPath, "nonexistent.mp3"); ++ ++ // Act ++ var result = PathHelper.ResolveUniquePath(path); ++ ++ // Assert ++ Assert.Equal(path, result); ++ } ++ ++ [Fact] ++ public void ResolveUniquePath_WhenFileExists_ReturnsPathWithCounter() ++ { ++ // Arrange ++ var basePath = Path.Combine(_testPath, "existing.mp3"); ++ File.WriteAllText(basePath, "content"); ++ ++ // Act ++ var result = PathHelper.ResolveUniquePath(basePath); ++ ++ // Assert ++ Assert.NotEqual(basePath, result); ++ Assert.Contains("existing (1).mp3", result); ++ } ++ ++ [Fact] ++ public void ResolveUniquePath_WhenMultipleFilesExist_IncrementsCounter() ++ { ++ // Arrange ++ var basePath = Path.Combine(_testPath, "song.mp3"); ++ var path1 = Path.Combine(_testPath, "song (1).mp3"); ++ File.WriteAllText(basePath, "content"); ++ File.WriteAllText(path1, "content"); ++ ++ // Act ++ var result = PathHelper.ResolveUniquePath(basePath); ++ ++ // Assert ++ Assert.Contains("song (2).mp3", result); ++ } ++ ++ #endregion ++} +diff --git a/allstarr.Tests/DeezerMetadataServiceTests.cs b/allstarr.Tests/DeezerMetadataServiceTests.cs +new file mode 100644 +index 0000000..222164c +--- /dev/null ++++ b/allstarr.Tests/DeezerMetadataServiceTests.cs +@@ -0,0 +1,805 @@ ++using allstarr.Services.Deezer; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using Moq; ++using Moq.Protected; ++using Microsoft.Extensions.Options; ++using System.Net; ++using System.Text.Json; ++ ++namespace allstarr.Tests; ++ ++public class DeezerMetadataServiceTests ++{ ++ private readonly Mock _httpClientFactoryMock; ++ private readonly Mock _httpMessageHandlerMock; ++ private readonly SubsonicSettings _settings; ++ private DeezerMetadataService _service; ++ ++ public DeezerMetadataServiceTests() ++ { ++ _httpMessageHandlerMock = new Mock(); ++ var httpClient = new HttpClient(_httpMessageHandlerMock.Object); ++ ++ _httpClientFactoryMock = new Mock(); ++ _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); ++ ++ _settings = new SubsonicSettings { ExplicitFilter = ExplicitFilter.ExplicitOnly }; ++ _service = CreateService(_settings); ++ } ++ ++ private DeezerMetadataService CreateService(SubsonicSettings settings) ++ { ++ var options = Options.Create(settings); ++ return new DeezerMetadataService(_httpClientFactoryMock.Object, options); ++ } ++ ++ [Fact] ++ public async Task SearchSongsAsync_ReturnsListOfSongs() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ data = new[] ++ { ++ new ++ { ++ id = 123456, ++ title = "Test Song", ++ duration = 180, ++ track_position = 1, ++ artist = new { id = 789, name = "Test Artist" }, ++ album = new { id = 456, title = "Test Album", cover_medium = "https://example.com/cover.jpg" } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchSongsAsync("test query", 20); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Single(result); ++ Assert.Equal("ext-deezer-song-123456", result[0].Id); ++ Assert.Equal("Test Song", result[0].Title); ++ Assert.Equal("Test Artist", result[0].Artist); ++ Assert.Equal("Test Album", result[0].Album); ++ Assert.Equal(180, result[0].Duration); ++ Assert.False(result[0].IsLocal); ++ Assert.Equal("deezer", result[0].ExternalProvider); ++ } ++ ++ [Fact] ++ public async Task SearchAlbumsAsync_ReturnsListOfAlbums() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ data = new[] ++ { ++ new ++ { ++ id = 456789, ++ title = "Test Album", ++ nb_tracks = 12, ++ release_date = "2023-01-15", ++ cover_medium = "https://example.com/album.jpg", ++ artist = new { id = 123, name = "Test Artist" } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchAlbumsAsync("test album", 20); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Single(result); ++ Assert.Equal("ext-deezer-album-456789", result[0].Id); ++ Assert.Equal("Test Album", result[0].Title); ++ Assert.Equal("Test Artist", result[0].Artist); ++ Assert.Equal(12, result[0].SongCount); ++ Assert.Equal(2023, result[0].Year); ++ Assert.False(result[0].IsLocal); ++ } ++ ++ [Fact] ++ public async Task SearchArtistsAsync_ReturnsListOfArtists() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ data = new[] ++ { ++ new ++ { ++ id = 789012, ++ name = "Test Artist", ++ nb_album = 5, ++ picture_medium = "https://example.com/artist.jpg" ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchArtistsAsync("test artist", 20); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Single(result); ++ Assert.Equal("ext-deezer-artist-789012", result[0].Id); ++ Assert.Equal("Test Artist", result[0].Name); ++ Assert.Equal(5, result[0].AlbumCount); ++ Assert.False(result[0].IsLocal); ++ } ++ ++ [Fact] ++ public async Task SearchAllAsync_ReturnsAllTypes() ++ { ++ // This test would need multiple HTTP calls mocked, simplified for now ++ var emptyResponse = JsonSerializer.Serialize(new { data = Array.Empty() }); ++ SetupHttpResponse(emptyResponse); ++ ++ // Act ++ var result = await _service.SearchAllAsync("test"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.NotNull(result.Songs); ++ Assert.NotNull(result.Albums); ++ Assert.NotNull(result.Artists); ++ } ++ ++ [Fact] ++ public async Task GetSongAsync_WithDeezerProvider_ReturnsSong() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ id = 123456, ++ title = "Test Song", ++ duration = 200, ++ track_position = 3, ++ artist = new { id = 789, name = "Test Artist" }, ++ album = new { id = 456, title = "Test Album", cover_medium = "https://example.com/cover.jpg" } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.GetSongAsync("deezer", "123456"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Equal("ext-deezer-song-123456", result.Id); ++ Assert.Equal("Test Song", result.Title); ++ } ++ ++ [Fact] ++ public async Task GetSongAsync_WithNonDeezerProvider_ReturnsNull() ++ { ++ // Act ++ var result = await _service.GetSongAsync("spotify", "123456"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ [Fact] ++ public async Task SearchSongsAsync_WithEmptyResponse_ReturnsEmptyList() ++ { ++ // Arrange ++ SetupHttpResponse(JsonSerializer.Serialize(new { data = Array.Empty() })); ++ ++ // Act ++ var result = await _service.SearchSongsAsync("nonexistent", 20); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Empty(result); ++ } ++ ++ [Fact] ++ public async Task SearchSongsAsync_WithHttpError_ReturnsEmptyList() ++ { ++ // Arrange ++ SetupHttpResponse("Error", HttpStatusCode.InternalServerError); ++ ++ // Act ++ var result = await _service.SearchSongsAsync("test", 20); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Empty(result); ++ } ++ ++ [Fact] ++ public async Task GetAlbumAsync_WithDeezerProvider_ReturnsAlbumWithTracks() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ id = 456789, ++ title = "Test Album", ++ nb_tracks = 2, ++ release_date = "2023-05-20", ++ cover_medium = "https://example.com/album.jpg", ++ artist = new { id = 123, name = "Test Artist" }, ++ tracks = new ++ { ++ data = new[] ++ { ++ new ++ { ++ id = 111, ++ title = "Track 1", ++ duration = 180, ++ track_position = 1, ++ artist = new { id = 123, name = "Test Artist" }, ++ album = new { id = 456789, title = "Test Album", cover_medium = "https://example.com/album.jpg" } ++ }, ++ new ++ { ++ id = 222, ++ title = "Track 2", ++ duration = 200, ++ track_position = 2, ++ artist = new { id = 123, name = "Test Artist" }, ++ album = new { id = 456789, title = "Test Album", cover_medium = "https://example.com/album.jpg" } ++ } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.GetAlbumAsync("deezer", "456789"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Equal("ext-deezer-album-456789", result.Id); ++ Assert.Equal("Test Album", result.Title); ++ Assert.Equal("Test Artist", result.Artist); ++ Assert.Equal(2, result.Songs.Count); ++ Assert.Equal("Track 1", result.Songs[0].Title); ++ Assert.Equal("Track 2", result.Songs[1].Title); ++ } ++ ++ [Fact] ++ public async Task GetAlbumAsync_WithNonDeezerProvider_ReturnsNull() ++ { ++ // Act ++ var result = await _service.GetAlbumAsync("spotify", "123456"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ private void SetupHttpResponse(string content, HttpStatusCode statusCode = HttpStatusCode.OK) ++ { ++ _httpMessageHandlerMock ++ .Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(new HttpResponseMessage ++ { ++ StatusCode = statusCode, ++ Content = new StringContent(content) ++ }); ++ } ++ ++ #region Explicit Filter Tests ++ ++ [Fact] ++ public async Task SearchSongsAsync_ExplicitOnlyFilter_ExcludesCleanVersions() ++ { ++ // Arrange ++ _service = CreateService(new SubsonicSettings { ExplicitFilter = ExplicitFilter.ExplicitOnly }); ++ ++ var deezerResponse = new ++ { ++ data = new object[] ++ { ++ new ++ { ++ id = 1, ++ title = "Explicit Original", ++ duration = 180, ++ explicit_content_lyrics = 1, // Explicit ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ }, ++ new ++ { ++ id = 2, ++ title = "Clean Version", ++ duration = 180, ++ explicit_content_lyrics = 3, // Clean/edited - should be excluded ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ }, ++ new ++ { ++ id = 3, ++ title = "Naturally Clean", ++ duration = 180, ++ explicit_content_lyrics = 0, // Naturally clean - should be included ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchSongsAsync("test", 20); ++ ++ // Assert ++ Assert.Equal(2, result.Count); ++ Assert.Contains(result, s => s.Title == "Explicit Original"); ++ Assert.Contains(result, s => s.Title == "Naturally Clean"); ++ Assert.DoesNotContain(result, s => s.Title == "Clean Version"); ++ } ++ ++ [Fact] ++ public async Task SearchSongsAsync_CleanOnlyFilter_ExcludesExplicitContent() ++ { ++ // Arrange ++ _service = CreateService(new SubsonicSettings { ExplicitFilter = ExplicitFilter.CleanOnly }); ++ ++ var deezerResponse = new ++ { ++ data = new object[] ++ { ++ new ++ { ++ id = 1, ++ title = "Explicit Original", ++ duration = 180, ++ explicit_content_lyrics = 1, // Explicit - should be excluded ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ }, ++ new ++ { ++ id = 2, ++ title = "Clean Version", ++ duration = 180, ++ explicit_content_lyrics = 3, // Clean/edited - should be included ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ }, ++ new ++ { ++ id = 3, ++ title = "Naturally Clean", ++ duration = 180, ++ explicit_content_lyrics = 0, // Naturally clean - should be included ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchSongsAsync("test", 20); ++ ++ // Assert ++ Assert.Equal(2, result.Count); ++ Assert.Contains(result, s => s.Title == "Clean Version"); ++ Assert.Contains(result, s => s.Title == "Naturally Clean"); ++ Assert.DoesNotContain(result, s => s.Title == "Explicit Original"); ++ } ++ ++ [Fact] ++ public async Task SearchSongsAsync_AllFilter_IncludesEverything() ++ { ++ // Arrange ++ _service = CreateService(new SubsonicSettings { ExplicitFilter = ExplicitFilter.All }); ++ ++ var deezerResponse = new ++ { ++ data = new object[] ++ { ++ new ++ { ++ id = 1, ++ title = "Explicit Original", ++ duration = 180, ++ explicit_content_lyrics = 1, ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ }, ++ new ++ { ++ id = 2, ++ title = "Clean Version", ++ duration = 180, ++ explicit_content_lyrics = 3, ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ }, ++ new ++ { ++ id = 3, ++ title = "Naturally Clean", ++ duration = 180, ++ explicit_content_lyrics = 0, ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchSongsAsync("test", 20); ++ ++ // Assert ++ Assert.Equal(3, result.Count); ++ } ++ ++ [Fact] ++ public async Task SearchSongsAsync_ExplicitOnlyFilter_IncludesTracksWithNoExplicitInfo() ++ { ++ // Arrange ++ _service = CreateService(new SubsonicSettings { ExplicitFilter = ExplicitFilter.ExplicitOnly }); ++ ++ var deezerResponse = new ++ { ++ data = new object[] ++ { ++ new ++ { ++ id = 1, ++ title = "No Explicit Info", ++ duration = 180, ++ // No explicit_content_lyrics field ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchSongsAsync("test", 20); ++ ++ // Assert ++ Assert.Single(result); ++ Assert.Equal("No Explicit Info", result[0].Title); ++ } ++ ++ [Fact] ++ public async Task GetAlbumAsync_ExplicitOnlyFilter_FiltersAlbumTracks() ++ { ++ // Arrange ++ _service = CreateService(new SubsonicSettings { ExplicitFilter = ExplicitFilter.ExplicitOnly }); ++ ++ var deezerResponse = new ++ { ++ id = 456789, ++ title = "Test Album", ++ nb_tracks = 3, ++ release_date = "2023-05-20", ++ cover_medium = "https://example.com/album.jpg", ++ artist = new { id = 123, name = "Test Artist" }, ++ tracks = new ++ { ++ data = new object[] ++ { ++ new ++ { ++ id = 111, ++ title = "Explicit Track", ++ duration = 180, ++ explicit_content_lyrics = 1, ++ artist = new { id = 123, name = "Test Artist" }, ++ album = new { id = 456789, title = "Test Album", cover_medium = "https://example.com/album.jpg" } ++ }, ++ new ++ { ++ id = 222, ++ title = "Clean Version Track", ++ duration = 200, ++ explicit_content_lyrics = 3, // Should be excluded ++ artist = new { id = 123, name = "Test Artist" }, ++ album = new { id = 456789, title = "Test Album", cover_medium = "https://example.com/album.jpg" } ++ }, ++ new ++ { ++ id = 333, ++ title = "Naturally Clean Track", ++ duration = 220, ++ explicit_content_lyrics = 0, ++ artist = new { id = 123, name = "Test Artist" }, ++ album = new { id = 456789, title = "Test Album", cover_medium = "https://example.com/album.jpg" } ++ } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.GetAlbumAsync("deezer", "456789"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Equal(2, result.Songs.Count); ++ Assert.Contains(result.Songs, s => s.Title == "Explicit Track"); ++ Assert.Contains(result.Songs, s => s.Title == "Naturally Clean Track"); ++ Assert.DoesNotContain(result.Songs, s => s.Title == "Clean Version Track"); ++ } ++ ++ [Fact] ++ public async Task SearchSongsAsync_ParsesExplicitContentLyrics() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ data = new object[] ++ { ++ new ++ { ++ id = 1, ++ title = "Test Track", ++ duration = 180, ++ explicit_content_lyrics = 1, ++ artist = new { id = 100, name = "Artist" }, ++ album = new { id = 200, title = "Album", cover_medium = "https://example.com/cover.jpg" } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchSongsAsync("test", 20); ++ ++ // Assert ++ Assert.Single(result); ++ Assert.Equal(1, result[0].ExplicitContentLyrics); ++ } ++ ++ #endregion ++ ++ #region Playlist Tests ++ ++ [Fact] ++ public async Task SearchPlaylistsAsync_ReturnsListOfPlaylists() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ data = new[] ++ { ++ new ++ { ++ id = 12345, ++ title = "Chill Vibes", ++ nb_tracks = 50, ++ picture_medium = "https://example.com/playlist1.jpg", ++ user = new { name = "Test User" } ++ }, ++ new ++ { ++ id = 67890, ++ title = "Workout Mix", ++ nb_tracks = 30, ++ picture_medium = "https://example.com/playlist2.jpg", ++ user = new { name = "Gym Buddy" } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchPlaylistsAsync("chill"); ++ ++ // Assert ++ Assert.Equal(2, result.Count); ++ Assert.Equal("Chill Vibes", result[0].Name); ++ Assert.Equal(50, result[0].TrackCount); ++ Assert.Equal("pl-deezer-12345", result[0].Id); ++ } ++ ++ [Fact] ++ public async Task SearchPlaylistsAsync_WithLimit_RespectsLimit() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ data = new[] ++ { ++ new ++ { ++ id = 12345, ++ title = "Playlist 1", ++ nb_tracks = 10, ++ picture_medium = "https://example.com/p1.jpg", ++ user = new { name = "User 1" } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchPlaylistsAsync("test", 1); ++ ++ // Assert ++ Assert.Single(result); ++ } ++ ++ [Fact] ++ public async Task SearchPlaylistsAsync_WithEmptyResults_ReturnsEmptyList() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ data = new object[] { } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.SearchPlaylistsAsync("nonexistent"); ++ ++ // Assert ++ Assert.Empty(result); ++ } ++ ++ [Fact] ++ public async Task GetPlaylistAsync_WithValidId_ReturnsPlaylist() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ id = 12345, ++ title = "Best Of Jazz", ++ description = "The best jazz tracks", ++ nb_tracks = 100, ++ picture_medium = "https://example.com/jazz.jpg", ++ user = new { name = "Jazz Lover" } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.GetPlaylistAsync("deezer", "12345"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Equal("Best Of Jazz", result.Name); ++ Assert.Equal(100, result.TrackCount); ++ Assert.Equal("pl-deezer-12345", result.Id); ++ } ++ ++ [Fact] ++ public async Task GetPlaylistAsync_WithWrongProvider_ReturnsNull() ++ { ++ // Act ++ var result = await _service.GetPlaylistAsync("qobuz", "12345"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ [Fact] ++ public async Task GetPlaylistTracksAsync_ReturnsListOfSongs() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ tracks = new ++ { ++ data = new[] ++ { ++ new ++ { ++ id = 111, ++ title = "Track 1", ++ duration = 200, ++ track_position = 1, ++ disk_number = 1, ++ artist = new ++ { ++ id = 999, ++ name = "Artist A" ++ }, ++ album = new ++ { ++ id = 888, ++ title = "Album X", ++ release_date = "2020-01-15", ++ cover_medium = "https://example.com/cover.jpg" ++ } ++ }, ++ new ++ { ++ id = 222, ++ title = "Track 2", ++ duration = 180, ++ track_position = 2, ++ disk_number = 1, ++ artist = new ++ { ++ id = 777, ++ name = "Artist B" ++ }, ++ album = new ++ { ++ id = 666, ++ title = "Album Y", ++ release_date = "2021-05-20", ++ cover_medium = "https://example.com/cover2.jpg" ++ } ++ } ++ } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.GetPlaylistTracksAsync("deezer", "12345"); ++ ++ // Assert ++ Assert.Equal(2, result.Count); ++ Assert.Equal("Track 1", result[0].Title); ++ Assert.Equal("Artist A", result[0].Artist); ++ Assert.Equal("ext-deezer-song-111", result[0].Id); ++ } ++ ++ [Fact] ++ public async Task GetPlaylistTracksAsync_WithWrongProvider_ReturnsEmptyList() ++ { ++ // Act ++ var result = await _service.GetPlaylistTracksAsync("qobuz", "12345"); ++ ++ // Assert ++ Assert.Empty(result); ++ } ++ ++ [Fact] ++ public async Task GetPlaylistTracksAsync_WithEmptyPlaylist_ReturnsEmptyList() ++ { ++ // Arrange ++ var deezerResponse = new ++ { ++ tracks = new ++ { ++ data = new object[] { } ++ } ++ }; ++ ++ SetupHttpResponse(JsonSerializer.Serialize(deezerResponse)); ++ ++ // Act ++ var result = await _service.GetPlaylistTracksAsync("deezer", "12345"); ++ ++ // Assert ++ Assert.Empty(result); ++ } ++ ++ #endregion ++} +diff --git a/allstarr.Tests/JellyfinModelMapperTests.cs b/allstarr.Tests/JellyfinModelMapperTests.cs +new file mode 100644 +index 0000000..4fdf9c5 +--- /dev/null ++++ b/allstarr.Tests/JellyfinModelMapperTests.cs +@@ -0,0 +1,401 @@ ++using Microsoft.Extensions.Logging; ++using Moq; ++using allstarr.Models.Domain; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using allstarr.Services.Jellyfin; ++using System.Text.Json; ++ ++namespace allstarr.Tests; ++ ++public class JellyfinModelMapperTests ++{ ++ private readonly JellyfinModelMapper _mapper; ++ private readonly JellyfinResponseBuilder _responseBuilder; ++ ++ public JellyfinModelMapperTests() ++ { ++ _responseBuilder = new JellyfinResponseBuilder(); ++ var mockLogger = new Mock>(); ++ _mapper = new JellyfinModelMapper(_responseBuilder, mockLogger.Object); ++ } ++ ++ [Fact] ++ public void ParseItemsResponse_AudioItems_ReturnsSongs() ++ { ++ // Arrange ++ var json = @"{ ++ ""Items"": [ ++ { ++ ""Id"": ""song-abc"", ++ ""Name"": ""Test Song"", ++ ""Type"": ""Audio"", ++ ""Album"": ""Test Album"", ++ ""AlbumId"": ""album-123"", ++ ""RunTimeTicks"": 2450000000, ++ ""IndexNumber"": 5, ++ ""ParentIndexNumber"": 1, ++ ""ProductionYear"": 2022, ++ ""Artists"": [""Test Artist""], ++ ""Genres"": [""Rock""] ++ } ++ ], ++ ""TotalRecordCount"": 1 ++ }"; ++ var doc = JsonDocument.Parse(json); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseItemsResponse(doc); ++ ++ // Assert ++ Assert.Single(songs); ++ Assert.Empty(albums); ++ Assert.Empty(artists); ++ ++ var song = songs[0]; ++ Assert.Equal("song-abc", song.Id); ++ Assert.Equal("Test Song", song.Title); ++ Assert.Equal("Test Album", song.Album); ++ Assert.Equal("Test Artist", song.Artist); ++ Assert.Equal(245, song.Duration); // 2450000000 ticks = 245 seconds ++ Assert.Equal(5, song.Track); ++ Assert.Equal(1, song.DiscNumber); ++ Assert.Equal(2022, song.Year); ++ Assert.Equal("Rock", song.Genre); ++ } ++ ++ [Fact] ++ public void ParseItemsResponse_AlbumItems_ReturnsAlbums() ++ { ++ // Arrange ++ var json = @"{ ++ ""Items"": [ ++ { ++ ""Id"": ""album-xyz"", ++ ""Name"": ""Greatest Hits"", ++ ""Type"": ""MusicAlbum"", ++ ""AlbumArtist"": ""Famous Band"", ++ ""ProductionYear"": 2020, ++ ""ChildCount"": 14, ++ ""Genres"": [""Pop""], ++ ""AlbumArtists"": [{""Id"": ""artist-1"", ""Name"": ""Famous Band""}] ++ } ++ ] ++ }"; ++ var doc = JsonDocument.Parse(json); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseItemsResponse(doc); ++ ++ // Assert ++ Assert.Empty(songs); ++ Assert.Single(albums); ++ Assert.Empty(artists); ++ ++ var album = albums[0]; ++ Assert.Equal("album-xyz", album.Id); ++ Assert.Equal("Greatest Hits", album.Title); ++ Assert.Equal("Famous Band", album.Artist); ++ Assert.Equal(2020, album.Year); ++ Assert.Equal(14, album.SongCount); ++ Assert.Equal("Pop", album.Genre); ++ } ++ ++ [Fact] ++ public void ParseItemsResponse_ArtistItems_ReturnsArtists() ++ { ++ // Arrange ++ var json = @"{ ++ ""Items"": [ ++ { ++ ""Id"": ""artist-999"", ++ ""Name"": ""The Rockers"", ++ ""Type"": ""MusicArtist"", ++ ""AlbumCount"": 7 ++ } ++ ] ++ }"; ++ var doc = JsonDocument.Parse(json); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseItemsResponse(doc); ++ ++ // Assert ++ Assert.Empty(songs); ++ Assert.Empty(albums); ++ Assert.Single(artists); ++ ++ var artist = artists[0]; ++ Assert.Equal("artist-999", artist.Id); ++ Assert.Equal("The Rockers", artist.Name); ++ Assert.Equal(7, artist.AlbumCount); ++ } ++ ++ [Fact] ++ public void ParseItemsResponse_MixedTypes_SortsCorrectly() ++ { ++ // Arrange ++ var json = @"{ ++ ""Items"": [ ++ {""Id"": ""1"", ""Name"": ""Song"", ""Type"": ""Audio""}, ++ {""Id"": ""2"", ""Name"": ""Album"", ""Type"": ""MusicAlbum""}, ++ {""Id"": ""3"", ""Name"": ""Artist"", ""Type"": ""MusicArtist""}, ++ {""Id"": ""4"", ""Name"": ""Another Song"", ""Type"": ""Audio""} ++ ] ++ }"; ++ var doc = JsonDocument.Parse(json); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseItemsResponse(doc); ++ ++ // Assert ++ Assert.Equal(2, songs.Count); ++ Assert.Single(albums); ++ Assert.Single(artists); ++ } ++ ++ [Fact] ++ public void ParseItemsResponse_NullResponse_ReturnsEmptyLists() ++ { ++ // Act ++ var (songs, albums, artists) = _mapper.ParseItemsResponse(null); ++ ++ // Assert ++ Assert.Empty(songs); ++ Assert.Empty(albums); ++ Assert.Empty(artists); ++ } ++ ++ [Fact] ++ public void ParseItemsResponse_EmptyItems_ReturnsEmptyLists() ++ { ++ // Arrange ++ var json = @"{""Items"": [], ""TotalRecordCount"": 0}"; ++ var doc = JsonDocument.Parse(json); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseItemsResponse(doc); ++ ++ // Assert ++ Assert.Empty(songs); ++ Assert.Empty(albums); ++ Assert.Empty(artists); ++ } ++ ++ [Fact] ++ public void ParseSong_ExtractsArtistFromAlbumArtist_WhenNoArtistsArray() ++ { ++ // Arrange ++ var json = @"{ ++ ""Id"": ""s1"", ++ ""Name"": ""Track"", ++ ""AlbumArtist"": ""Fallback Artist"" ++ }"; ++ var element = JsonDocument.Parse(json).RootElement; ++ ++ // Act ++ var song = _mapper.ParseSong(element); ++ ++ // Assert ++ Assert.Equal("Fallback Artist", song.Artist); ++ } ++ ++ [Fact] ++ public void ParseSong_ExtractsArtistId_FromArtistItems() ++ { ++ // Arrange ++ var json = @"{ ++ ""Id"": ""s1"", ++ ""Name"": ""Track"", ++ ""Artists"": [""Main Artist""], ++ ""ArtistItems"": [{""Id"": ""art-id-123"", ""Name"": ""Main Artist""}] ++ }"; ++ var element = JsonDocument.Parse(json).RootElement; ++ ++ // Act ++ var song = _mapper.ParseSong(element); ++ ++ // Assert ++ Assert.Equal("art-id-123", song.ArtistId); ++ Assert.Equal("Main Artist", song.Artist); ++ } ++ ++ [Fact] ++ public void ParseAlbum_ExtractsArtistId_FromAlbumArtists() ++ { ++ // Arrange ++ var json = @"{ ++ ""Id"": ""alb-1"", ++ ""Name"": ""The Album"", ++ ""AlbumArtist"": ""Band Name"", ++ ""AlbumArtists"": [{""Id"": ""band-id"", ""Name"": ""Band Name""}] ++ }"; ++ var element = JsonDocument.Parse(json).RootElement; ++ ++ // Act ++ var album = _mapper.ParseAlbum(element); ++ ++ // Assert ++ Assert.Equal("band-id", album.ArtistId); ++ } ++ ++ [Fact] ++ public void MergeSearchResults_DeduplicatesArtistsByName() ++ { ++ // Arrange ++ var localArtists = new List ++ { ++ new() { Id = "local-1", Name = "The Beatles", IsLocal = true } ++ }; ++ ++ var externalResult = new SearchResult ++ { ++ Songs = new List(), ++ Albums = new List(), ++ Artists = new List ++ { ++ new() { Id = "ext-deezer-artist-1", Name = "The Beatles", IsLocal = false }, ++ new() { Id = "ext-deezer-artist-2", Name = "Pink Floyd", IsLocal = false } ++ } ++ }; ++ ++ var playlists = new List(); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.MergeSearchResults( ++ new List(), new List(), localArtists, externalResult, playlists); ++ ++ // Assert - Beatles should not be duplicated, Pink Floyd should be added ++ Assert.Equal(2, artists.Count); ++ Assert.Contains(artists, a => a["Id"]!.ToString() == "local-1"); ++ Assert.Contains(artists, a => a["Id"]!.ToString() == "ext-deezer-artist-2"); ++ } ++ ++ [Fact] ++ public void MergeSearchResults_IncludesPlaylistsAsAlbums() ++ { ++ // Arrange ++ var playlists = new List ++ { ++ new() { Id = "pl-1", Name = "Summer Mix", Provider = "deezer", ExternalId = "123" } ++ }; ++ ++ var externalResult = new SearchResult ++ { ++ Songs = new List(), ++ Albums = new List(), ++ Artists = new List() ++ }; ++ ++ // Act ++ var (songs, albums, artists) = _mapper.MergeSearchResults( ++ new List(), new List(), new List(), externalResult, playlists); ++ ++ // Assert ++ Assert.Single(albums); ++ Assert.Equal("pl-1", albums[0]["Id"]); ++ } ++ ++ [Fact] ++ public void ParseAlbumWithTracks_CombinesAlbumAndTracks() ++ { ++ // Arrange ++ var albumJson = @"{ ++ ""Id"": ""album-1"", ++ ""Name"": ""Test Album"", ++ ""Type"": ""MusicAlbum"", ++ ""AlbumArtist"": ""Test Artist"" ++ }"; ++ var tracksJson = @"{ ++ ""Items"": [ ++ {""Id"": ""t1"", ""Name"": ""Track 1"", ""Type"": ""Audio""}, ++ {""Id"": ""t2"", ""Name"": ""Track 2"", ""Type"": ""Audio""} ++ ] ++ }"; ++ ++ var albumDoc = JsonDocument.Parse(albumJson); ++ var tracksDoc = JsonDocument.Parse(tracksJson); ++ ++ // Act ++ var album = _mapper.ParseAlbumWithTracks(albumDoc, tracksDoc); ++ ++ // Assert ++ Assert.NotNull(album); ++ Assert.Equal("album-1", album.Id); ++ Assert.Equal(2, album.Songs.Count); ++ } ++ ++ [Fact] ++ public void ParseAlbumWithTracks_NullAlbum_ReturnsNull() ++ { ++ // Act ++ var album = _mapper.ParseAlbumWithTracks(null, null); ++ ++ // Assert ++ Assert.Null(album); ++ } ++ ++ [Fact] ++ public void ParseArtistWithAlbums_SetsAlbumCount() ++ { ++ // Arrange ++ var artistJson = @"{ ++ ""Id"": ""art-1"", ++ ""Name"": ""Test Artist"", ++ ""Type"": ""MusicArtist"" ++ }"; ++ var albumsJson = @"{ ++ ""Items"": [ ++ {""Id"": ""a1"", ""Name"": ""Album 1""}, ++ {""Id"": ""a2"", ""Name"": ""Album 2""}, ++ {""Id"": ""a3"", ""Name"": ""Album 3""} ++ ] ++ }"; ++ ++ var artistDoc = JsonDocument.Parse(artistJson); ++ var albumsDoc = JsonDocument.Parse(albumsJson); ++ ++ // Act ++ var artist = _mapper.ParseArtistWithAlbums(artistDoc, albumsDoc); ++ ++ // Assert ++ Assert.NotNull(artist); ++ Assert.Equal("art-1", artist.Id); ++ Assert.Equal(3, artist.AlbumCount); ++ } ++ ++ [Fact] ++ public void ParseSearchHintsResponse_HandlesSearchHintsFormat() ++ { ++ // Arrange ++ var json = @"{ ++ ""SearchHints"": [ ++ {""Id"": ""s1"", ""Name"": ""Song"", ""Type"": ""Audio"", ""Album"": ""Album"", ""AlbumArtist"": ""Artist""}, ++ {""Id"": ""a1"", ""Name"": ""Album"", ""Type"": ""MusicAlbum"", ""AlbumArtist"": ""Artist""}, ++ {""Id"": ""ar1"", ""Name"": ""Artist"", ""Type"": ""MusicArtist""} ++ ], ++ ""TotalRecordCount"": 3 ++ }"; ++ var doc = JsonDocument.Parse(json); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseSearchHintsResponse(doc); ++ ++ // Assert ++ Assert.Single(songs); ++ Assert.Single(albums); ++ Assert.Single(artists); ++ } ++ ++ [Fact] ++ public void ParseSearchHintsResponse_NullResponse_ReturnsEmptyLists() ++ { ++ // Act ++ var (songs, albums, artists) = _mapper.ParseSearchHintsResponse(null); ++ ++ // Assert ++ Assert.Empty(songs); ++ Assert.Empty(albums); ++ Assert.Empty(artists); ++ } ++} +diff --git a/allstarr.Tests/JellyfinProxyServiceTests.cs b/allstarr.Tests/JellyfinProxyServiceTests.cs +new file mode 100644 +index 0000000..4c47390 +--- /dev/null ++++ b/allstarr.Tests/JellyfinProxyServiceTests.cs +@@ -0,0 +1,434 @@ ++using Microsoft.AspNetCore.Http; ++using Microsoft.AspNetCore.Mvc; ++using Microsoft.Extensions.Logging; ++using Microsoft.Extensions.Options; ++using Moq; ++using Moq.Protected; ++using allstarr.Models.Settings; ++using allstarr.Services.Jellyfin; ++using System.Net; ++using System.Text.Json; ++ ++namespace allstarr.Tests; ++ ++public class JellyfinProxyServiceTests ++{ ++ private readonly JellyfinProxyService _service; ++ private readonly Mock _mockHandler; ++ private readonly Mock _mockHttpClientFactory; ++ private readonly JellyfinSettings _settings; ++ ++ public JellyfinProxyServiceTests() ++ { ++ _mockHandler = new Mock(); ++ var httpClient = new HttpClient(_mockHandler.Object); ++ ++ _mockHttpClientFactory = new Mock(); ++ _mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(httpClient); ++ ++ _settings = new JellyfinSettings ++ { ++ Url = "http://localhost:8096", ++ ApiKey = "test-api-key-12345", ++ UserId = "user-guid-here", ++ ClientName = "TestClient", ++ DeviceName = "TestDevice", ++ DeviceId = "test-device-id", ++ ClientVersion = "1.0.0" ++ }; ++ ++ var httpContext = new DefaultHttpContext(); ++ var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; ++ var mockLogger = new Mock>(); ++ ++ _service = new JellyfinProxyService( ++ _mockHttpClientFactory.Object, ++ Options.Create(_settings), ++ httpContextAccessor, ++ mockLogger.Object); ++ } ++ ++ [Fact] ++ public async Task GetJsonAsync_ValidResponse_ReturnsJsonDocument() ++ { ++ // Arrange ++ var jsonResponse = "{\"Items\":[{\"Id\":\"123\",\"Name\":\"Test Song\"}],\"TotalRecordCount\":1}"; ++ SetupMockResponse(HttpStatusCode.OK, jsonResponse, "application/json"); ++ ++ // Act ++ var result = await _service.GetJsonAsync("Items"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.True(result.RootElement.TryGetProperty("Items", out var items)); ++ Assert.Equal(1, items.GetArrayLength()); ++ } ++ ++ [Fact] ++ public async Task GetJsonAsync_ServerError_ReturnsNull() ++ { ++ // Arrange ++ SetupMockResponse(HttpStatusCode.InternalServerError, "", "text/plain"); ++ ++ // Act ++ var result = await _service.GetJsonAsync("Items"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ [Fact] ++ public async Task GetJsonAsync_IncludesAuthHeader() ++ { ++ // Arrange ++ HttpRequestMessage? captured = null; ++ _mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .Callback((req, ct) => captured = req) ++ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new StringContent("{}") ++ }); ++ ++ // Act ++ await _service.GetJsonAsync("Items"); ++ ++ // Assert ++ Assert.NotNull(captured); ++ Assert.True(captured!.Headers.Contains("Authorization")); ++ var authHeader = captured.Headers.GetValues("Authorization").First(); ++ Assert.Contains("MediaBrowser", authHeader); ++ Assert.Contains(_settings.ApiKey, authHeader); ++ Assert.Contains(_settings.ClientName, authHeader); ++ } ++ ++ [Fact] ++ public async Task GetBytesAsync_ReturnsBodyAndContentType() ++ { ++ // Arrange ++ var imageBytes = new byte[] { 0x89, 0x50, 0x4E, 0x47 }; // PNG magic bytes ++ var response = new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new ByteArrayContent(imageBytes) ++ }; ++ response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/png"); ++ ++ _mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(response); ++ ++ // Act ++ var (body, contentType) = await _service.GetBytesAsync("Items/123/Images/Primary"); ++ ++ // Assert ++ Assert.Equal(imageBytes, body); ++ Assert.Equal("image/png", contentType); ++ } ++ ++ [Fact] ++ public async Task GetBytesSafeAsync_OnError_ReturnsSuccessFalse() ++ { ++ // Arrange ++ _mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ThrowsAsync(new HttpRequestException("Connection refused")); ++ ++ // Act ++ var (body, contentType, success) = await _service.GetBytesSafeAsync("Items/123/Images/Primary"); ++ ++ // Assert ++ Assert.False(success); ++ Assert.Null(body); ++ Assert.Null(contentType); ++ } ++ ++ [Fact] ++ public async Task SearchAsync_BuildsCorrectQueryParams() ++ { ++ // Arrange ++ HttpRequestMessage? captured = null; ++ _mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .Callback((req, ct) => captured = req) ++ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new StringContent("{\"Items\":[],\"TotalRecordCount\":0}") ++ }); ++ ++ // Act ++ await _service.SearchAsync("test query", new[] { "Audio", "MusicAlbum" }, 25); ++ ++ // Assert ++ Assert.NotNull(captured); ++ var url = captured!.RequestUri!.ToString(); ++ Assert.Contains("searchTerm=test%20query", url); ++ Assert.Contains("includeItemTypes=Audio%2CMusicAlbum", url); ++ Assert.Contains("limit=25", url); ++ Assert.Contains("recursive=true", url); ++ } ++ ++ [Fact] ++ public async Task GetItemAsync_RequestsCorrectEndpoint() ++ { ++ // Arrange ++ HttpRequestMessage? captured = null; ++ var itemJson = "{\"Id\":\"abc-123\",\"Name\":\"My Song\",\"Type\":\"Audio\"}"; ++ _mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .Callback((req, ct) => captured = req) ++ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new StringContent(itemJson) ++ }); ++ ++ // Act ++ var result = await _service.GetItemAsync("abc-123"); ++ ++ // Assert ++ Assert.NotNull(captured); ++ Assert.Contains("/Items/abc-123", captured!.RequestUri!.ToString()); ++ Assert.NotNull(result); ++ } ++ ++ [Fact] ++ public async Task GetArtistsAsync_WithSearchTerm_IncludesInQuery() ++ { ++ // Arrange ++ HttpRequestMessage? captured = null; ++ _mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .Callback((req, ct) => captured = req) ++ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new StringContent("{\"Items\":[],\"TotalRecordCount\":0}") ++ }); ++ ++ // Act ++ await _service.GetArtistsAsync("Beatles", 10); ++ ++ // Assert ++ Assert.NotNull(captured); ++ var url = captured!.RequestUri!.ToString(); ++ Assert.Contains("/Artists", url); ++ Assert.Contains("searchTerm=Beatles", url); ++ Assert.Contains("limit=10", url); ++ } ++ ++ [Fact] ++ public async Task GetImageAsync_WithDimensions_IncludesMaxWidthHeight() ++ { ++ // Arrange ++ HttpRequestMessage? captured = null; ++ _mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .Callback((req, ct) => captured = req) ++ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new ByteArrayContent(new byte[] { 1, 2, 3 }) ++ }); ++ ++ // Act ++ await _service.GetImageAsync("item-123", "Primary", maxWidth: 300, maxHeight: 300); ++ ++ // Assert ++ Assert.NotNull(captured); ++ var url = captured!.RequestUri!.ToString(); ++ Assert.Contains("/Items/item-123/Images/Primary", url); ++ Assert.Contains("maxWidth=300", url); ++ Assert.Contains("maxHeight=300", url); ++ } ++ ++ [Fact] ++ public async Task MarkFavoriteAsync_PostsToCorrectEndpoint() ++ { ++ // Arrange ++ HttpRequestMessage? captured = null; ++ _mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .Callback((req, ct) => captured = req) ++ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); ++ ++ // Act ++ var result = await _service.MarkFavoriteAsync("song-456"); ++ ++ // Assert ++ Assert.True(result); ++ Assert.NotNull(captured); ++ Assert.Equal(HttpMethod.Post, captured!.Method); ++ Assert.Contains($"/Users/{_settings.UserId}/FavoriteItems/song-456", captured.RequestUri!.ToString()); ++ } ++ ++ [Fact] ++ public async Task MarkFavoriteAsync_WithoutUserId_ReturnsFalse() ++ { ++ // Arrange - create service without UserId ++ var settingsWithoutUser = new JellyfinSettings ++ { ++ Url = "http://localhost:8096", ++ ApiKey = "test-key", ++ UserId = "" // no user ++ }; ++ ++ var httpContext = new DefaultHttpContext(); ++ var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; ++ var mockLogger = new Mock>(); ++ ++ var service = new JellyfinProxyService( ++ _mockHttpClientFactory.Object, ++ Options.Create(settingsWithoutUser), ++ httpContextAccessor, ++ mockLogger.Object); ++ ++ // Act ++ var result = await service.MarkFavoriteAsync("song-456"); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ [Fact] ++ public async Task TestConnectionAsync_ValidServer_ReturnsSuccess() ++ { ++ // Arrange ++ var serverInfo = "{\"ServerName\":\"My Jellyfin\",\"Version\":\"10.8.0\"}"; ++ SetupMockResponse(HttpStatusCode.OK, serverInfo, "application/json"); ++ ++ // Act ++ var (success, serverName, version) = await _service.TestConnectionAsync(); ++ ++ // Assert ++ Assert.True(success); ++ Assert.Equal("My Jellyfin", serverName); ++ Assert.Equal("10.8.0", version); ++ } ++ ++ [Fact] ++ public async Task TestConnectionAsync_ServerDown_ReturnsFalse() ++ { ++ // Arrange ++ _mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ThrowsAsync(new HttpRequestException("Connection refused")); ++ ++ // Act ++ var (success, serverName, version) = await _service.TestConnectionAsync(); ++ ++ // Assert ++ Assert.False(success); ++ Assert.Null(serverName); ++ Assert.Null(version); ++ } ++ ++ [Fact] ++ public async Task GetMusicLibraryIdAsync_WhenConfigured_ReturnsConfiguredId() ++ { ++ // Arrange - settings already have LibraryId set ++ var settingsWithLibrary = new JellyfinSettings ++ { ++ Url = "http://localhost:8096", ++ ApiKey = "test-key", ++ LibraryId = "configured-library-id" ++ }; ++ ++ var httpContext = new DefaultHttpContext(); ++ var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; ++ var mockLogger = new Mock>(); ++ ++ var service = new JellyfinProxyService( ++ _mockHttpClientFactory.Object, ++ Options.Create(settingsWithLibrary), ++ httpContextAccessor, ++ mockLogger.Object); ++ ++ // Act ++ var result = await service.GetMusicLibraryIdAsync(); ++ ++ // Assert ++ Assert.Equal("configured-library-id", result); ++ } ++ ++ [Fact] ++ public async Task GetMusicLibraryIdAsync_AutoDetects_MusicLibrary() ++ { ++ // Arrange ++ var librariesJson = "{\"Items\":[{\"Id\":\"video-lib\",\"CollectionType\":\"movies\"},{\"Id\":\"music-lib-123\",\"CollectionType\":\"music\"}]}"; ++ SetupMockResponse(HttpStatusCode.OK, librariesJson, "application/json"); ++ ++ var settingsNoLibrary = new JellyfinSettings ++ { ++ Url = "http://localhost:8096", ++ ApiKey = "test-key", ++ LibraryId = "" // not configured ++ }; ++ ++ var httpContext = new DefaultHttpContext(); ++ var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; ++ var mockLogger = new Mock>(); ++ ++ var service = new JellyfinProxyService( ++ _mockHttpClientFactory.Object, ++ Options.Create(settingsNoLibrary), ++ httpContextAccessor, ++ mockLogger.Object); ++ ++ // Act ++ var result = await service.GetMusicLibraryIdAsync(); ++ ++ // Assert ++ Assert.Equal("music-lib-123", result); ++ } ++ ++ [Fact] ++ public async Task StreamAudioAsync_NullContext_ReturnsError() ++ { ++ // Arrange ++ var httpContextAccessor = new HttpContextAccessor { HttpContext = null }; ++ var mockLogger = new Mock>(); ++ ++ var service = new JellyfinProxyService( ++ _mockHttpClientFactory.Object, ++ Options.Create(_settings), ++ httpContextAccessor, ++ mockLogger.Object); ++ ++ // Act ++ var result = await service.StreamAudioAsync("song-123", CancellationToken.None); ++ ++ // Assert ++ var objectResult = Assert.IsType(result); ++ Assert.Equal(500, objectResult.StatusCode); ++ } ++ ++ private void SetupMockResponse(HttpStatusCode statusCode, string content, string contentType) ++ { ++ var response = new HttpResponseMessage(statusCode) ++ { ++ Content = new StringContent(content) ++ }; ++ response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); ++ ++ _mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(response); ++ } ++} +diff --git a/allstarr.Tests/JellyfinResponseBuilderTests.cs b/allstarr.Tests/JellyfinResponseBuilderTests.cs +new file mode 100644 +index 0000000..0fce213 +--- /dev/null ++++ b/allstarr.Tests/JellyfinResponseBuilderTests.cs +@@ -0,0 +1,292 @@ ++using Microsoft.AspNetCore.Mvc; ++using allstarr.Models.Domain; ++using allstarr.Models.Subsonic; ++using allstarr.Services.Jellyfin; ++ ++namespace allstarr.Tests; ++ ++public class JellyfinResponseBuilderTests ++{ ++ private readonly JellyfinResponseBuilder _builder; ++ ++ public JellyfinResponseBuilderTests() ++ { ++ _builder = new JellyfinResponseBuilder(); ++ } ++ ++ [Fact] ++ public void ConvertSongToJellyfinItem_SetsCorrectFields() ++ { ++ // Arrange ++ var song = new Song ++ { ++ Id = "song-123", ++ Title = "Test Track", ++ Artist = "Test Artist", ++ Album = "Test Album", ++ AlbumId = "album-456", ++ ArtistId = "artist-789", ++ Duration = 245, ++ Track = 3, ++ DiscNumber = 1, ++ Year = 2023, ++ Genre = "Rock", ++ IsLocal = true ++ }; ++ ++ // Act ++ var result = _builder.ConvertSongToJellyfinItem(song); ++ ++ // Assert ++ Assert.Equal("song-123", result["Id"]); ++ Assert.Equal("Test Track", result["Name"]); ++ Assert.Equal("Audio", result["Type"]); ++ Assert.Equal("Test Album", result["Album"]); ++ Assert.Equal("album-456", result["AlbumId"]); ++ Assert.Equal(3, result["IndexNumber"]); ++ Assert.Equal(1, result["ParentIndexNumber"]); ++ Assert.Equal(2023, result["ProductionYear"]); ++ Assert.Equal(245 * TimeSpan.TicksPerSecond, result["RunTimeTicks"]); ++ } ++ ++ [Fact] ++ public void ConvertSongToJellyfinItem_ExternalSong_IncludesProviderIds() ++ { ++ // Arrange ++ var song = new Song ++ { ++ Id = "ext-deezer-song-12345", ++ Title = "External Track", ++ Artist = "External Artist", ++ IsLocal = false, ++ ExternalProvider = "deezer", ++ ExternalId = "12345", ++ Isrc = "USRC12345678" ++ }; ++ ++ // Act ++ var result = _builder.ConvertSongToJellyfinItem(song); ++ ++ // Assert ++ Assert.True(result.ContainsKey("ProviderIds")); ++ var providerIds = result["ProviderIds"] as Dictionary; ++ Assert.NotNull(providerIds); ++ Assert.Equal("12345", providerIds["deezer"]); ++ Assert.Equal("USRC12345678", providerIds["ISRC"]); ++ } ++ ++ [Fact] ++ public void ConvertAlbumToJellyfinItem_SetsCorrectFields() ++ { ++ // Arrange ++ var album = new Album ++ { ++ Id = "album-456", ++ Title = "Greatest Hits", ++ Artist = "Famous Band", ++ ArtistId = "artist-123", ++ Year = 2020, ++ SongCount = 12, ++ Genre = "Pop", ++ IsLocal = true ++ }; ++ ++ // Act ++ var result = _builder.ConvertAlbumToJellyfinItem(album); ++ ++ // Assert ++ Assert.Equal("album-456", result["Id"]); ++ Assert.Equal("Greatest Hits", result["Name"]); ++ Assert.Equal("MusicAlbum", result["Type"]); ++ Assert.Equal(true, result["IsFolder"]); ++ Assert.Equal("Famous Band", result["AlbumArtist"]); ++ Assert.Equal(2020, result["ProductionYear"]); ++ Assert.Equal(12, result["ChildCount"]); ++ } ++ ++ [Fact] ++ public void ConvertArtistToJellyfinItem_SetsCorrectFields() ++ { ++ // Arrange ++ var artist = new Artist ++ { ++ Id = "artist-789", ++ Name = "The Rockers", ++ AlbumCount = 5, ++ IsLocal = true ++ }; ++ ++ // Act ++ var result = _builder.ConvertArtistToJellyfinItem(artist); ++ ++ // Assert ++ Assert.Equal("artist-789", result["Id"]); ++ Assert.Equal("The Rockers", result["Name"]); ++ Assert.Equal("MusicArtist", result["Type"]); ++ Assert.Equal(true, result["IsFolder"]); ++ Assert.Equal(5, result["AlbumCount"]); ++ } ++ ++ [Fact] ++ public void ConvertPlaylistToAlbumItem_SetsPlaylistType() ++ { ++ // Arrange ++ var playlist = new ExternalPlaylist ++ { ++ Id = "ext-playlist-deezer-999", ++ ExternalId = "999", ++ Name = "Summer Vibes", ++ Provider = "deezer", ++ CuratorName = "DJ Cool", ++ TrackCount = 50, ++ Duration = 3600, ++ CreatedDate = new DateTime(2023, 6, 15) ++ }; ++ ++ // Act ++ var result = _builder.ConvertPlaylistToAlbumItem(playlist); ++ ++ // Assert ++ Assert.Equal("ext-playlist-deezer-999", result["Id"]); ++ Assert.Equal("Summer Vibes", result["Name"]); ++ Assert.Equal("Playlist", result["Type"]); ++ Assert.Equal("DJ Cool", result["AlbumArtist"]); ++ Assert.Equal(50, result["ChildCount"]); ++ Assert.Equal(2023, result["ProductionYear"]); ++ } ++ ++ [Fact] ++ public void ConvertPlaylistToAlbumItem_NoCurator_UsesProvider() ++ { ++ // Arrange ++ var playlist = new ExternalPlaylist ++ { ++ Id = "ext-playlist-deezer-888", ++ ExternalId = "888", ++ Name = "Top Hits", ++ Provider = "deezer", ++ CuratorName = null, ++ TrackCount = 30 ++ }; ++ ++ // Act ++ var result = _builder.ConvertPlaylistToAlbumItem(playlist); ++ ++ // Assert ++ Assert.Equal("deezer", result["AlbumArtist"]); ++ } ++ ++ [Fact] ++ public void CreateItemsResponse_ReturnsPaginatedResult() ++ { ++ // Arrange ++ var songs = new List ++ { ++ new() { Id = "1", Title = "Song One", Artist = "Artist", Duration = 200 }, ++ new() { Id = "2", Title = "Song Two", Artist = "Artist", Duration = 180 } ++ }; ++ ++ // Act ++ var result = _builder.CreateItemsResponse(songs); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ Assert.NotNull(jsonResult.Value); ++ } ++ ++ [Fact] ++ public void CreateSearchHintsResponse_IncludesAllTypes() ++ { ++ // Arrange ++ var songs = new List { new() { Id = "s1", Title = "Track", Artist = "A" } }; ++ var albums = new List { new() { Id = "a1", Title = "Album", Artist = "A" } }; ++ var artists = new List { new() { Id = "ar1", Name = "Artist" } }; ++ ++ // Act ++ var result = _builder.CreateSearchHintsResponse(songs, albums, artists); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ Assert.NotNull(jsonResult.Value); ++ } ++ ++ [Fact] ++ public void CreateError_Returns404ForNotFound() ++ { ++ // Act ++ var result = _builder.CreateError(404, "Item not found"); ++ ++ // Assert ++ var objectResult = Assert.IsType(result); ++ Assert.Equal(404, objectResult.StatusCode); ++ } ++ ++ [Fact] ++ public void CreateAlbumResponse_IncludesChildrenForSongs() ++ { ++ // Arrange ++ var album = new Album ++ { ++ Id = "album-1", ++ Title = "Full Album", ++ Artist = "Artist", ++ Songs = new List ++ { ++ new() { Id = "t1", Title = "Track 1", Artist = "Artist", Track = 1 }, ++ new() { Id = "t2", Title = "Track 2", Artist = "Artist", Track = 2 } ++ } ++ }; ++ ++ // Act ++ var result = _builder.CreateAlbumResponse(album); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ Assert.NotNull(jsonResult.Value); ++ } ++ ++ [Fact] ++ public void CreateArtistResponse_IncludesAlbumsList() ++ { ++ // Arrange ++ var artist = new Artist { Id = "art-1", Name = "Test Artist" }; ++ var albums = new List ++ { ++ new() { Id = "alb-1", Title = "First Album", Artist = "Test Artist" }, ++ new() { Id = "alb-2", Title = "Second Album", Artist = "Test Artist" } ++ }; ++ ++ // Act ++ var result = _builder.CreateArtistResponse(artist, albums); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ Assert.NotNull(jsonResult.Value); ++ } ++ ++ [Fact] ++ public void CreatePlaylistAsAlbumResponse_CalculatesTotalDuration() ++ { ++ // Arrange ++ var playlist = new ExternalPlaylist ++ { ++ Id = "pl-1", ++ Name = "My Playlist", ++ Provider = "deezer", ++ ExternalId = "123" ++ }; ++ var tracks = new List ++ { ++ new() { Id = "t1", Title = "Song 1", Duration = 180 }, ++ new() { Id = "t2", Title = "Song 2", Duration = 240 }, ++ new() { Id = "t3", Title = "Song 3", Duration = 200 } ++ }; ++ ++ // Act ++ var result = _builder.CreatePlaylistAsAlbumResponse(playlist, tracks); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ Assert.NotNull(jsonResult.Value); ++ } ++} +diff --git a/allstarr.Tests/LocalLibraryServiceTests.cs b/allstarr.Tests/LocalLibraryServiceTests.cs +new file mode 100644 +index 0000000..ac49c6a +--- /dev/null ++++ b/allstarr.Tests/LocalLibraryServiceTests.cs +@@ -0,0 +1,248 @@ ++using allstarr.Services.Local; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using Microsoft.Extensions.Configuration; ++using Microsoft.Extensions.Logging; ++using Microsoft.Extensions.Options; ++using Moq; ++using Moq.Protected; ++using System.Net; ++ ++namespace allstarr.Tests; ++ ++public class LocalLibraryServiceTests : IDisposable ++{ ++ private readonly LocalLibraryService _service; ++ private readonly string _testDownloadPath; ++ private readonly Mock _mockHttpClientFactory; ++ ++ public LocalLibraryServiceTests() ++ { ++ _testDownloadPath = Path.Combine(Path.GetTempPath(), "allstarr-tests-" + Guid.NewGuid()); ++ Directory.CreateDirectory(_testDownloadPath); ++ ++ var configuration = new ConfigurationBuilder() ++ .AddInMemoryCollection(new Dictionary ++ { ++ ["Library:DownloadPath"] = _testDownloadPath ++ }) ++ .Build(); ++ ++ // Mock HttpClient ++ var mockHandler = new Mock(); ++ mockHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new StringContent("{\"subsonic-response\":{\"status\":\"ok\",\"scanStatus\":{\"scanning\":false,\"count\":100}}}") ++ }); ++ ++ var httpClient = new HttpClient(mockHandler.Object); ++ _mockHttpClientFactory = new Mock(); ++ _mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(httpClient); ++ ++ var subsonicSettings = Options.Create(new SubsonicSettings { Url = "http://localhost:4533" }); ++ var mockLogger = new Mock>(); ++ ++ _service = new LocalLibraryService(configuration, _mockHttpClientFactory.Object, subsonicSettings, mockLogger.Object); ++ } ++ ++ public void Dispose() ++ { ++ if (Directory.Exists(_testDownloadPath)) ++ { ++ Directory.Delete(_testDownloadPath, true); ++ } ++ } ++ ++ [Fact] ++ public void ParseSongId_WithExternalId_ReturnsCorrectParts() ++ { ++ // Act ++ var (isExternal, provider, externalId) = _service.ParseSongId("ext-deezer-123456"); ++ ++ // Assert ++ Assert.True(isExternal); ++ Assert.Equal("deezer", provider); ++ Assert.Equal("123456", externalId); ++ } ++ ++ [Fact] ++ public void ParseSongId_WithLocalId_ReturnsNotExternal() ++ { ++ // Act ++ var (isExternal, provider, externalId) = _service.ParseSongId("local-789"); ++ ++ // Assert ++ Assert.False(isExternal); ++ Assert.Null(provider); ++ Assert.Null(externalId); ++ } ++ ++ [Fact] ++ public void ParseSongId_WithNumericId_ReturnsNotExternal() ++ { ++ // Act ++ var (isExternal, provider, externalId) = _service.ParseSongId("12345"); ++ ++ // Assert ++ Assert.False(isExternal); ++ Assert.Null(provider); ++ Assert.Null(externalId); ++ } ++ ++ [Fact] ++ public async Task GetLocalPathForExternalSongAsync_WhenNotRegistered_ReturnsNull() ++ { ++ // Act ++ var result = await _service.GetLocalPathForExternalSongAsync("deezer", "nonexistent"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ [Fact] ++ public async Task RegisterDownloadedSongAsync_ThenGetLocalPath_ReturnsPath() ++ { ++ // Arrange ++ var song = new Song ++ { ++ Id = "ext-deezer-123456", ++ Title = "Test Song", ++ Artist = "Test Artist", ++ Album = "Test Album", ++ ExternalProvider = "deezer", ++ ExternalId = "123456" ++ }; ++ var localPath = Path.Combine(_testDownloadPath, "test-song.mp3"); ++ ++ // Create the file ++ await File.WriteAllTextAsync(localPath, "fake audio content"); ++ ++ // Act ++ await _service.RegisterDownloadedSongAsync(song, localPath); ++ var result = await _service.GetLocalPathForExternalSongAsync("deezer", "123456"); ++ ++ // Assert ++ Assert.Equal(localPath, result); ++ } ++ ++ [Fact] ++ public async Task GetLocalPathForExternalSongAsync_WhenFileDeleted_ReturnsNull() ++ { ++ // Arrange ++ var song = new Song ++ { ++ Id = "ext-deezer-999999", ++ Title = "Deleted Song", ++ Artist = "Test Artist", ++ Album = "Test Album", ++ ExternalProvider = "deezer", ++ ExternalId = "999999" ++ }; ++ var localPath = Path.Combine(_testDownloadPath, "deleted-song.mp3"); ++ ++ // Create and then delete the file ++ await File.WriteAllTextAsync(localPath, "fake audio content"); ++ await _service.RegisterDownloadedSongAsync(song, localPath); ++ File.Delete(localPath); ++ ++ // Act ++ var result = await _service.GetLocalPathForExternalSongAsync("deezer", "999999"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ [Fact] ++ public async Task RegisterDownloadedSongAsync_WithNullProvider_DoesNothing() ++ { ++ // Arrange ++ var song = new Song ++ { ++ Id = "local-123", ++ Title = "Local Song", ++ Artist = "Local Artist", ++ Album = "Local Album", ++ ExternalProvider = null, ++ ExternalId = null ++ }; ++ var localPath = Path.Combine(_testDownloadPath, "local-song.mp3"); ++ ++ // Act - should not throw ++ await _service.RegisterDownloadedSongAsync(song, localPath); ++ ++ // Assert - nothing to assert, just checking it doesn't throw ++ Assert.True(true); ++ } ++ ++ [Fact] ++ public async Task TriggerLibraryScanAsync_ReturnsTrue() ++ { ++ // Act ++ var result = await _service.TriggerLibraryScanAsync(); ++ ++ // Assert ++ Assert.True(result); ++ } ++ ++ [Fact] ++ public async Task GetScanStatusAsync_ReturnsScanStatus() ++ { ++ // Act ++ var result = await _service.GetScanStatusAsync(); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.False(result.Scanning); ++ Assert.Equal(100, result.Count); ++ } ++ ++ [Theory] ++ [InlineData("ext-deezer-123", true, "deezer", "123")] ++ [InlineData("ext-spotify-abc123", true, "spotify", "abc123")] ++ [InlineData("ext-tidal-999-888", true, "tidal", "999-888")] ++ [InlineData("ext-deezer-song-123456", true, "deezer", "123456")] // New format - extracts numeric ID ++ [InlineData("123456", false, null, null)] ++ [InlineData("", false, null, null)] ++ [InlineData("ext-", false, null, null)] ++ [InlineData("ext-deezer", false, null, null)] ++ public void ParseSongId_VariousInputs_ReturnsExpected(string songId, bool expectedIsExternal, string? expectedProvider, string? expectedExternalId) ++ { ++ // Act ++ var (isExternal, provider, externalId) = _service.ParseSongId(songId); ++ ++ // Assert ++ Assert.Equal(expectedIsExternal, isExternal); ++ Assert.Equal(expectedProvider, provider); ++ Assert.Equal(expectedExternalId, externalId); ++ } ++ ++ [Theory] ++ [InlineData("ext-deezer-song-123456", true, "deezer", "song", "123456")] ++ [InlineData("ext-deezer-album-789012", true, "deezer", "album", "789012")] ++ [InlineData("ext-deezer-artist-259", true, "deezer", "artist", "259")] ++ [InlineData("ext-spotify-song-abc123", true, "spotify", "song", "abc123")] ++ [InlineData("ext-deezer-123", true, "deezer", "song", "123")] // Legacy format defaults to song ++ [InlineData("ext-tidal-999", true, "tidal", "song", "999")] // Legacy format defaults to song ++ [InlineData("123456", false, null, null, null)] ++ [InlineData("", false, null, null, null)] ++ [InlineData("ext-", false, null, null, null)] ++ [InlineData("ext-deezer", false, null, null, null)] ++ public void ParseExternalId_VariousInputs_ReturnsExpected(string id, bool expectedIsExternal, string? expectedProvider, string? expectedType, string? expectedExternalId) ++ { ++ // Act ++ var (isExternal, provider, type, externalId) = _service.ParseExternalId(id); ++ ++ // Assert ++ Assert.Equal(expectedIsExternal, isExternal); ++ Assert.Equal(expectedProvider, provider); ++ Assert.Equal(expectedType, type); ++ Assert.Equal(expectedExternalId, externalId); ++ } ++} +diff --git a/allstarr.Tests/PlaylistIdHelperTests.cs b/allstarr.Tests/PlaylistIdHelperTests.cs +new file mode 100644 +index 0000000..53e1c95 +--- /dev/null ++++ b/allstarr.Tests/PlaylistIdHelperTests.cs +@@ -0,0 +1,375 @@ ++using allstarr.Services.Common; ++using Xunit; ++ ++namespace allstarr.Tests; ++ ++public class PlaylistIdHelperTests ++{ ++ #region IsExternalPlaylist Tests ++ ++ [Fact] ++ public void IsExternalPlaylist_WithValidPlaylistId_ReturnsTrue() ++ { ++ // Arrange ++ var id = "pl-deezer-123456"; ++ ++ // Act ++ var result = PlaylistIdHelper.IsExternalPlaylist(id); ++ ++ // Assert ++ Assert.True(result); ++ } ++ ++ [Fact] ++ public void IsExternalPlaylist_WithValidQobuzPlaylistId_ReturnsTrue() ++ { ++ // Arrange ++ var id = "pl-qobuz-789012"; ++ ++ // Act ++ var result = PlaylistIdHelper.IsExternalPlaylist(id); ++ ++ // Assert ++ Assert.True(result); ++ } ++ ++ [Fact] ++ public void IsExternalPlaylist_WithUpperCasePrefix_ReturnsTrue() ++ { ++ // Arrange ++ var id = "PL-deezer-123456"; ++ ++ // Act ++ var result = PlaylistIdHelper.IsExternalPlaylist(id); ++ ++ // Assert ++ Assert.True(result); ++ } ++ ++ [Fact] ++ public void IsExternalPlaylist_WithRegularAlbumId_ReturnsFalse() ++ { ++ // Arrange ++ var id = "ext-deezer-album-123456"; ++ ++ // Act ++ var result = PlaylistIdHelper.IsExternalPlaylist(id); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ [Fact] ++ public void IsExternalPlaylist_WithNullId_ReturnsFalse() ++ { ++ // Arrange ++ string? id = null; ++ ++ // Act ++ var result = PlaylistIdHelper.IsExternalPlaylist(id); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ [Fact] ++ public void IsExternalPlaylist_WithEmptyString_ReturnsFalse() ++ { ++ // Arrange ++ var id = ""; ++ ++ // Act ++ var result = PlaylistIdHelper.IsExternalPlaylist(id); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ [Fact] ++ public void IsExternalPlaylist_WithRandomString_ReturnsFalse() ++ { ++ // Arrange ++ var id = "random-string-123"; ++ ++ // Act ++ var result = PlaylistIdHelper.IsExternalPlaylist(id); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ #endregion ++ ++ #region ParsePlaylistId Tests ++ ++ [Fact] ++ public void ParsePlaylistId_WithValidDeezerPlaylistId_ReturnsProviderAndExternalId() ++ { ++ // Arrange ++ var id = "pl-deezer-123456"; ++ ++ // Act ++ var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(id); ++ ++ // Assert ++ Assert.Equal("deezer", provider); ++ Assert.Equal("123456", externalId); ++ } ++ ++ [Fact] ++ public void ParsePlaylistId_WithValidQobuzPlaylistId_ReturnsProviderAndExternalId() ++ { ++ // Arrange ++ var id = "pl-qobuz-789012"; ++ ++ // Act ++ var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(id); ++ ++ // Assert ++ Assert.Equal("qobuz", provider); ++ Assert.Equal("789012", externalId); ++ } ++ ++ [Fact] ++ public void ParsePlaylistId_WithExternalIdContainingDashes_ParsesCorrectly() ++ { ++ // Arrange ++ var id = "pl-deezer-abc-def-123"; ++ ++ // Act ++ var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(id); ++ ++ // Assert ++ Assert.Equal("deezer", provider); ++ Assert.Equal("abc-def-123", externalId); ++ } ++ ++ [Fact] ++ public void ParsePlaylistId_WithInvalidFormatNoProvider_ThrowsArgumentException() ++ { ++ // Arrange ++ var id = "pl-123456"; ++ ++ // Act & Assert ++ var exception = Assert.Throws(() => PlaylistIdHelper.ParsePlaylistId(id)); ++ Assert.Contains("Invalid playlist ID format", exception.Message); ++ } ++ ++ [Fact] ++ public void ParsePlaylistId_WithNonPlaylistId_ThrowsArgumentException() ++ { ++ // Arrange ++ var id = "ext-deezer-album-123456"; ++ ++ // Act & Assert ++ var exception = Assert.Throws(() => PlaylistIdHelper.ParsePlaylistId(id)); ++ Assert.Contains("Invalid playlist ID format", exception.Message); ++ } ++ ++ [Fact] ++ public void ParsePlaylistId_WithNullId_ThrowsArgumentException() ++ { ++ // Arrange ++ string? id = null; ++ ++ // Act & Assert ++ Assert.Throws(() => PlaylistIdHelper.ParsePlaylistId(id!)); ++ } ++ ++ [Fact] ++ public void ParsePlaylistId_WithEmptyString_ThrowsArgumentException() ++ { ++ // Arrange ++ var id = ""; ++ ++ // Act & Assert ++ Assert.Throws(() => PlaylistIdHelper.ParsePlaylistId(id)); ++ } ++ ++ [Fact] ++ public void ParsePlaylistId_WithOnlyPrefix_ThrowsArgumentException() ++ { ++ // Arrange ++ var id = "pl-"; ++ ++ // Act & Assert ++ var exception = Assert.Throws(() => PlaylistIdHelper.ParsePlaylistId(id)); ++ Assert.Contains("Invalid playlist ID format", exception.Message); ++ } ++ ++ #endregion ++ ++ #region CreatePlaylistId Tests ++ ++ [Fact] ++ public void CreatePlaylistId_WithValidDeezerProviderAndId_ReturnsCorrectFormat() ++ { ++ // Arrange ++ var provider = "deezer"; ++ var externalId = "123456"; ++ ++ // Act ++ var result = PlaylistIdHelper.CreatePlaylistId(provider, externalId); ++ ++ // Assert ++ Assert.Equal("pl-deezer-123456", result); ++ } ++ ++ [Fact] ++ public void CreatePlaylistId_WithValidQobuzProviderAndId_ReturnsCorrectFormat() ++ { ++ // Arrange ++ var provider = "qobuz"; ++ var externalId = "789012"; ++ ++ // Act ++ var result = PlaylistIdHelper.CreatePlaylistId(provider, externalId); ++ ++ // Assert ++ Assert.Equal("pl-qobuz-789012", result); ++ } ++ ++ [Fact] ++ public void CreatePlaylistId_WithUpperCaseProvider_ConvertsToLowerCase() ++ { ++ // Arrange ++ var provider = "DEEZER"; ++ var externalId = "123456"; ++ ++ // Act ++ var result = PlaylistIdHelper.CreatePlaylistId(provider, externalId); ++ ++ // Assert ++ Assert.Equal("pl-deezer-123456", result); ++ } ++ ++ [Fact] ++ public void CreatePlaylistId_WithMixedCaseProvider_ConvertsToLowerCase() ++ { ++ // Arrange ++ var provider = "DeEzEr"; ++ var externalId = "123456"; ++ ++ // Act ++ var result = PlaylistIdHelper.CreatePlaylistId(provider, externalId); ++ ++ // Assert ++ Assert.Equal("pl-deezer-123456", result); ++ } ++ ++ [Fact] ++ public void CreatePlaylistId_WithExternalIdContainingDashes_PreservesDashes() ++ { ++ // Arrange ++ var provider = "deezer"; ++ var externalId = "abc-def-123"; ++ ++ // Act ++ var result = PlaylistIdHelper.CreatePlaylistId(provider, externalId); ++ ++ // Assert ++ Assert.Equal("pl-deezer-abc-def-123", result); ++ } ++ ++ [Fact] ++ public void CreatePlaylistId_WithNullProvider_ThrowsArgumentException() ++ { ++ // Arrange ++ string? provider = null; ++ var externalId = "123456"; ++ ++ // Act & Assert ++ var exception = Assert.Throws(() => PlaylistIdHelper.CreatePlaylistId(provider!, externalId)); ++ Assert.Contains("Provider cannot be null or empty", exception.Message); ++ } ++ ++ [Fact] ++ public void CreatePlaylistId_WithEmptyProvider_ThrowsArgumentException() ++ { ++ // Arrange ++ var provider = ""; ++ var externalId = "123456"; ++ ++ // Act & Assert ++ var exception = Assert.Throws(() => PlaylistIdHelper.CreatePlaylistId(provider, externalId)); ++ Assert.Contains("Provider cannot be null or empty", exception.Message); ++ } ++ ++ [Fact] ++ public void CreatePlaylistId_WithNullExternalId_ThrowsArgumentException() ++ { ++ // Arrange ++ var provider = "deezer"; ++ string? externalId = null; ++ ++ // Act & Assert ++ var exception = Assert.Throws(() => PlaylistIdHelper.CreatePlaylistId(provider, externalId!)); ++ Assert.Contains("External ID cannot be null or empty", exception.Message); ++ } ++ ++ [Fact] ++ public void CreatePlaylistId_WithEmptyExternalId_ThrowsArgumentException() ++ { ++ // Arrange ++ var provider = "deezer"; ++ var externalId = ""; ++ ++ // Act & Assert ++ var exception = Assert.Throws(() => PlaylistIdHelper.CreatePlaylistId(provider, externalId)); ++ Assert.Contains("External ID cannot be null or empty", exception.Message); ++ } ++ ++ #endregion ++ ++ #region Round-Trip Tests ++ ++ [Fact] ++ public void RoundTrip_CreateAndParse_ReturnsOriginalValues() ++ { ++ // Arrange ++ var originalProvider = "deezer"; ++ var originalExternalId = "123456"; ++ ++ // Act ++ var playlistId = PlaylistIdHelper.CreatePlaylistId(originalProvider, originalExternalId); ++ var (parsedProvider, parsedExternalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); ++ ++ // Assert ++ Assert.Equal(originalProvider, parsedProvider); ++ Assert.Equal(originalExternalId, parsedExternalId); ++ } ++ ++ [Fact] ++ public void RoundTrip_CreateWithUpperCaseAndParse_ReturnsLowerCaseProvider() ++ { ++ // Arrange ++ var originalProvider = "QOBUZ"; ++ var originalExternalId = "789012"; ++ ++ // Act ++ var playlistId = PlaylistIdHelper.CreatePlaylistId(originalProvider, originalExternalId); ++ var (parsedProvider, parsedExternalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); ++ ++ // Assert ++ Assert.Equal("qobuz", parsedProvider); // Converted to lowercase ++ Assert.Equal(originalExternalId, parsedExternalId); ++ } ++ ++ [Fact] ++ public void RoundTrip_WithComplexExternalId_PreservesValue() ++ { ++ // Arrange ++ var originalProvider = "deezer"; ++ var originalExternalId = "abc-123-def-456"; ++ ++ // Act ++ var playlistId = PlaylistIdHelper.CreatePlaylistId(originalProvider, originalExternalId); ++ var (parsedProvider, parsedExternalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); ++ ++ // Assert ++ Assert.Equal(originalProvider, parsedProvider); ++ Assert.Equal(originalExternalId, parsedExternalId); ++ } ++ ++ #endregion ++} +diff --git a/allstarr.Tests/QobuzDownloadServiceTests.cs b/allstarr.Tests/QobuzDownloadServiceTests.cs +new file mode 100644 +index 0000000..14f91c0 +--- /dev/null ++++ b/allstarr.Tests/QobuzDownloadServiceTests.cs +@@ -0,0 +1,389 @@ ++using allstarr.Services; ++using allstarr.Services.Qobuz; ++using allstarr.Services.Local; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Subsonic; ++using Microsoft.Extensions.Configuration; ++using Microsoft.Extensions.Logging; ++using Microsoft.Extensions.Options; ++using Moq; ++using Moq.Protected; ++using System.Net; ++ ++namespace allstarr.Tests; ++ ++public class QobuzDownloadServiceTests : IDisposable ++{ ++ private readonly Mock _httpClientFactoryMock; ++ private readonly Mock _httpMessageHandlerMock; ++ private readonly Mock _localLibraryServiceMock; ++ private readonly Mock _metadataServiceMock; ++ private readonly Mock> _bundleServiceLoggerMock; ++ private readonly Mock> _loggerMock; ++ private readonly IConfiguration _configuration; ++ private readonly string _testDownloadPath; ++ private QobuzBundleService _bundleService; ++ ++ public QobuzDownloadServiceTests() ++ { ++ _testDownloadPath = Path.Combine(Path.GetTempPath(), "allstarr-qobuz-tests-" + Guid.NewGuid()); ++ Directory.CreateDirectory(_testDownloadPath); ++ ++ _httpMessageHandlerMock = new Mock(); ++ var httpClient = new HttpClient(_httpMessageHandlerMock.Object); ++ ++ _httpClientFactoryMock = new Mock(); ++ _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); ++ ++ _localLibraryServiceMock = new Mock(); ++ _metadataServiceMock = new Mock(); ++ _bundleServiceLoggerMock = new Mock>(); ++ _loggerMock = new Mock>(); ++ ++ // Create a real QobuzBundleService for testing (it will use the mocked HttpClient) ++ _bundleService = new QobuzBundleService(_httpClientFactoryMock.Object, _bundleServiceLoggerMock.Object); ++ ++ _configuration = new ConfigurationBuilder() ++ .AddInMemoryCollection(new Dictionary ++ { ++ ["Library:DownloadPath"] = _testDownloadPath ++ }) ++ .Build(); ++ } ++ ++ public void Dispose() ++ { ++ if (Directory.Exists(_testDownloadPath)) ++ { ++ Directory.Delete(_testDownloadPath, true); ++ } ++ } ++ ++ private QobuzDownloadService CreateService( ++ string? userAuthToken = null, ++ string? userId = null, ++ string? quality = null, ++ DownloadMode downloadMode = DownloadMode.Track) ++ { ++ var config = new ConfigurationBuilder() ++ .AddInMemoryCollection(new Dictionary ++ { ++ ["Library:DownloadPath"] = _testDownloadPath ++ }) ++ .Build(); ++ ++ var subsonicSettings = Options.Create(new SubsonicSettings ++ { ++ DownloadMode = downloadMode ++ }); ++ ++ var qobuzSettings = Options.Create(new QobuzSettings ++ { ++ UserAuthToken = userAuthToken, ++ UserId = userId, ++ Quality = quality ++ }); ++ ++ var serviceProviderMock = new Mock(); ++ serviceProviderMock.Setup(sp => sp.GetService(typeof(allstarr.Services.Subsonic.PlaylistSyncService))) ++ .Returns(null); ++ ++ return new QobuzDownloadService( ++ _httpClientFactoryMock.Object, ++ config, ++ _localLibraryServiceMock.Object, ++ _metadataServiceMock.Object, ++ _bundleService, ++ subsonicSettings, ++ qobuzSettings, ++ serviceProviderMock.Object, ++ _loggerMock.Object); ++ } ++ ++ #region IsAvailableAsync Tests ++ ++ [Fact] ++ public async Task IsAvailableAsync_WithoutUserAuthToken_ReturnsFalse() ++ { ++ // Arrange ++ var service = CreateService(userAuthToken: null, userId: "123"); ++ ++ // Act ++ var result = await service.IsAvailableAsync(); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ [Fact] ++ public async Task IsAvailableAsync_WithoutUserId_ReturnsFalse() ++ { ++ // Arrange ++ var service = CreateService(userAuthToken: "test-token", userId: null); ++ ++ // Act ++ var result = await service.IsAvailableAsync(); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ [Fact] ++ public async Task IsAvailableAsync_WithEmptyCredentials_ReturnsFalse() ++ { ++ // Arrange ++ var service = CreateService(userAuthToken: "", userId: ""); ++ ++ // Act ++ var result = await service.IsAvailableAsync(); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ [Fact] ++ public async Task IsAvailableAsync_WithValidCredentials_WhenBundleServiceWorks_ReturnsTrue() ++ { ++ // Arrange ++ // Mock a successful response for bundle service ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.OK, ++ Content = new StringContent(@"") ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.Is(req => req.RequestUri!.ToString().Contains("qobuz.com")), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ var service = CreateService(userAuthToken: "test-token", userId: "123"); ++ ++ // Act ++ var result = await service.IsAvailableAsync(); ++ ++ // Assert - Will be false because bundle extraction will fail with our mock, but service is constructed ++ Assert.False(result); ++ } ++ ++ [Fact] ++ public async Task IsAvailableAsync_WhenBundleServiceFails_ReturnsFalse() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.ServiceUnavailable ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ var service = CreateService(userAuthToken: "test-token", userId: "123"); ++ ++ // Act ++ var result = await service.IsAvailableAsync(); ++ ++ // Assert ++ Assert.False(result); ++ } ++ ++ #endregion ++ ++ #region DownloadSongAsync Tests ++ ++ [Fact] ++ public async Task DownloadSongAsync_WithUnsupportedProvider_ThrowsNotSupportedException() ++ { ++ // Arrange ++ var service = CreateService(userAuthToken: "test-token", userId: "123"); ++ ++ // Act & Assert ++ await Assert.ThrowsAsync(() => ++ service.DownloadSongAsync("spotify", "123456")); ++ } ++ ++ [Fact] ++ public async Task DownloadSongAsync_WhenAlreadyDownloaded_ReturnsExistingPath() ++ { ++ // Arrange ++ var existingPath = Path.Combine(_testDownloadPath, "existing-song.flac"); ++ await File.WriteAllTextAsync(existingPath, "fake audio content"); ++ ++ _localLibraryServiceMock ++ .Setup(s => s.GetLocalPathForExternalSongAsync("qobuz", "123456")) ++ .ReturnsAsync(existingPath); ++ ++ var service = CreateService(userAuthToken: "test-token", userId: "123"); ++ ++ // Act ++ var result = await service.DownloadSongAsync("qobuz", "123456"); ++ ++ // Assert ++ Assert.Equal(existingPath, result); ++ } ++ ++ [Fact] ++ public async Task DownloadSongAsync_WhenSongNotFound_ThrowsException() ++ { ++ // Arrange ++ _localLibraryServiceMock ++ .Setup(s => s.GetLocalPathForExternalSongAsync("qobuz", "999999")) ++ .ReturnsAsync((string?)null); ++ ++ _metadataServiceMock ++ .Setup(s => s.GetSongAsync("qobuz", "999999")) ++ .ReturnsAsync((Song?)null); ++ ++ var service = CreateService(userAuthToken: "test-token", userId: "123"); ++ ++ // Act & Assert ++ var exception = await Assert.ThrowsAsync(() => ++ service.DownloadSongAsync("qobuz", "999999")); ++ ++ Assert.Equal("Song not found", exception.Message); ++ } ++ ++ #endregion ++ ++ #region GetDownloadStatus Tests ++ ++ [Fact] ++ public void GetDownloadStatus_WithUnknownSongId_ReturnsNull() ++ { ++ // Arrange ++ var service = CreateService(userAuthToken: "test-token", userId: "123"); ++ ++ // Act ++ var result = service.GetDownloadStatus("unknown-id"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ #endregion ++ ++ #region Album Download Tests ++ ++ [Fact] ++ public void DownloadRemainingAlbumTracksInBackground_WithUnsupportedProvider_DoesNotThrow() ++ { ++ // Arrange ++ var service = CreateService( ++ userAuthToken: "test-token", ++ userId: "123", ++ downloadMode: DownloadMode.Album); ++ ++ // Act & Assert - Should not throw, just log warning ++ service.DownloadRemainingAlbumTracksInBackground("spotify", "123456", "789"); ++ } ++ ++ [Fact] ++ public void DownloadRemainingAlbumTracksInBackground_WithQobuzProvider_StartsBackgroundTask() ++ { ++ // Arrange ++ _metadataServiceMock ++ .Setup(s => s.GetAlbumAsync("qobuz", "123456")) ++ .ReturnsAsync(new Album ++ { ++ Id = "ext-qobuz-album-123456", ++ Title = "Test Album", ++ Songs = new List ++ { ++ new Song { ExternalId = "111", Title = "Track 1" }, ++ new Song { ExternalId = "222", Title = "Track 2" } ++ } ++ }); ++ ++ var service = CreateService( ++ userAuthToken: "test-token", ++ userId: "123", ++ downloadMode: DownloadMode.Album); ++ ++ // Act - Should not throw (fire-and-forget) ++ service.DownloadRemainingAlbumTracksInBackground("qobuz", "123456", "111"); ++ ++ // Assert - Just verify it doesn't throw, actual download is async ++ Assert.True(true); ++ } ++ ++ #endregion ++ ++ #region ExtractExternalIdFromAlbumId Tests ++ ++ [Fact] ++ public void ExtractExternalIdFromAlbumId_WithValidQobuzAlbumId_ReturnsExternalId() ++ { ++ // Arrange ++ var service = CreateService(userAuthToken: "test-token", userId: "123"); ++ var albumId = "ext-qobuz-album-0060253780838"; ++ ++ // Act ++ // We need to use reflection to test this protected method, or test it indirectly ++ // For now, we'll test it indirectly through DownloadRemainingAlbumTracksInBackground ++ _metadataServiceMock ++ .Setup(s => s.GetAlbumAsync("qobuz", "0060253780838")) ++ .ReturnsAsync(new Album ++ { ++ Id = albumId, ++ Title = "Test Album", ++ Songs = new List() ++ }); ++ ++ // Assert - If this doesn't throw, the extraction worked ++ service.DownloadRemainingAlbumTracksInBackground("qobuz", albumId, "track-1"); ++ Assert.True(true); ++ } ++ ++ #endregion ++ ++ #region Quality Format Tests ++ ++ [Fact] ++ public async Task CreateService_WithFlacQuality_UsesCorrectFormat() ++ { ++ // Arrange & Act ++ var service = CreateService( ++ userAuthToken: "test-token", ++ userId: "123", ++ quality: "FLAC"); ++ ++ // Assert - Service created successfully with quality setting ++ Assert.NotNull(service); ++ } ++ ++ [Fact] ++ public async Task CreateService_WithMp3Quality_UsesCorrectFormat() ++ { ++ // Arrange & Act ++ var service = CreateService( ++ userAuthToken: "test-token", ++ userId: "123", ++ quality: "MP3_320"); ++ ++ // Assert - Service created successfully with quality setting ++ Assert.NotNull(service); ++ } ++ ++ [Fact] ++ public async Task CreateService_WithNullQuality_UsesDefaultFormat() ++ { ++ // Arrange & Act ++ var service = CreateService( ++ userAuthToken: "test-token", ++ userId: "123", ++ quality: null); ++ ++ // Assert - Service created successfully with default quality ++ Assert.NotNull(service); ++ } ++ ++ #endregion ++} +diff --git a/allstarr.Tests/QobuzMetadataServiceTests.cs b/allstarr.Tests/QobuzMetadataServiceTests.cs +new file mode 100644 +index 0000000..1086d38 +--- /dev/null ++++ b/allstarr.Tests/QobuzMetadataServiceTests.cs +@@ -0,0 +1,662 @@ ++using allstarr.Services.Qobuz; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Subsonic; ++using Microsoft.Extensions.Logging; ++using Microsoft.Extensions.Options; ++using Moq; ++using Moq.Protected; ++using System.Net; ++ ++namespace allstarr.Tests; ++ ++public class QobuzMetadataServiceTests ++{ ++ private readonly Mock _httpClientFactoryMock; ++ private readonly Mock _httpMessageHandlerMock; ++ private readonly Mock _bundleServiceMock; ++ private readonly Mock> _loggerMock; ++ private readonly QobuzMetadataService _service; ++ ++ public QobuzMetadataServiceTests() ++ { ++ _httpMessageHandlerMock = new Mock(); ++ var httpClient = new HttpClient(_httpMessageHandlerMock.Object); ++ ++ _httpClientFactoryMock = new Mock(); ++ _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); ++ ++ // Mock QobuzBundleService (methods are now virtual so can be mocked) ++ var bundleHttpClientFactoryMock = new Mock(); ++ bundleHttpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); ++ var bundleLogger = Mock.Of>(); ++ _bundleServiceMock = new Mock(bundleHttpClientFactoryMock.Object, bundleLogger) { CallBase = false }; ++ _bundleServiceMock.Setup(b => b.GetAppIdAsync()).ReturnsAsync("fake-app-id-12345"); ++ _bundleServiceMock.Setup(b => b.GetSecretsAsync()).ReturnsAsync(new List { "fake-secret" }); ++ _bundleServiceMock.Setup(b => b.GetSecretAsync(It.IsAny())).ReturnsAsync("fake-secret"); ++ ++ _loggerMock = new Mock>(); ++ ++ var subsonicSettings = Options.Create(new SubsonicSettings()); ++ var qobuzSettings = Options.Create(new QobuzSettings ++ { ++ UserAuthToken = "fake-user-auth-token", ++ UserId = "8807208" ++ }); ++ ++ _service = new QobuzMetadataService( ++ _httpClientFactoryMock.Object, ++ subsonicSettings, ++ qobuzSettings, ++ _bundleServiceMock.Object, ++ _loggerMock.Object); ++ } ++ ++ #region SearchPlaylistsAsync Tests ++ ++ [Fact] ++ public async Task SearchPlaylistsAsync_WithValidQuery_ReturnsPlaylists() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.OK, ++ Content = new StringContent(@"{ ++ ""playlists"": { ++ ""items"": [ ++ { ++ ""id"": 1578664, ++ ""name"": ""Jazz Classics"", ++ ""description"": ""Best of classic jazz music"", ++ ""tracks_count"": 50, ++ ""duration"": 12000, ++ ""owner"": { ++ ""name"": ""Qobuz Editorial"" ++ }, ++ ""created_at"": 1609459200, ++ ""images300"": [""https://example.com/cover.jpg""] ++ } ++ ] ++ } ++ }") ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.SearchPlaylistsAsync("jazz", 20); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Single(result); ++ Assert.Equal("Jazz Classics", result[0].Name); ++ Assert.Equal("Best of classic jazz music", result[0].Description); ++ Assert.Equal(50, result[0].TrackCount); ++ Assert.Equal(12000, result[0].Duration); ++ Assert.Equal("qobuz", result[0].Provider); ++ Assert.Equal("1578664", result[0].ExternalId); ++ Assert.Equal("pl-qobuz-1578664", result[0].Id); ++ Assert.Equal("Qobuz Editorial", result[0].CuratorName); ++ } ++ ++ [Fact] ++ public async Task SearchPlaylistsAsync_WithEmptyResults_ReturnsEmptyList() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.OK, ++ Content = new StringContent(@"{ ++ ""playlists"": { ++ ""items"": [] ++ } ++ }") ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.SearchPlaylistsAsync("nonexistent", 20); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Empty(result); ++ } ++ ++ [Fact] ++ public async Task SearchPlaylistsAsync_WhenHttpFails_ReturnsEmptyList() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.InternalServerError ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.SearchPlaylistsAsync("jazz", 20); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Empty(result); ++ } ++ ++ #endregion ++ ++ #region GetPlaylistAsync Tests ++ ++ [Fact] ++ public async Task GetPlaylistAsync_WithValidId_ReturnsPlaylist() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.OK, ++ Content = new StringContent(@"{ ++ ""id"": 1578664, ++ ""name"": ""Best Of Jazz"", ++ ""description"": ""Top jazz tracks"", ++ ""tracks_count"": 100, ++ ""duration"": 24000, ++ ""owner"": { ++ ""name"": ""Qobuz Editor"" ++ }, ++ ""created_at"": 1609459200, ++ ""image_rectangle"": [""https://example.com/cover-large.jpg""] ++ }") ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.GetPlaylistAsync("qobuz", "1578664"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Equal("Best Of Jazz", result.Name); ++ Assert.Equal("Top jazz tracks", result.Description); ++ Assert.Equal(100, result.TrackCount); ++ Assert.Equal(24000, result.Duration); ++ Assert.Equal("pl-qobuz-1578664", result.Id); ++ Assert.Equal("Qobuz Editor", result.CuratorName); ++ Assert.Equal("https://example.com/cover-large.jpg", result.CoverUrl); ++ } ++ ++ [Fact] ++ public async Task GetPlaylistAsync_WithWrongProvider_ReturnsNull() ++ { ++ // Act ++ var result = await _service.GetPlaylistAsync("deezer", "12345"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ #endregion ++ ++ #region GetPlaylistTracksAsync Tests ++ ++ [Fact] ++ public async Task GetPlaylistTracksAsync_WithValidId_ReturnsTracks() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.OK, ++ Content = new StringContent(@"{ ++ ""id"": 1578664, ++ ""name"": ""My Jazz Playlist"", ++ ""tracks"": { ++ ""items"": [ ++ { ++ ""id"": 123456789, ++ ""title"": ""Take Five"", ++ ""duration"": 324, ++ ""track_number"": 1, ++ ""media_number"": 1, ++ ""performer"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ }, ++ ""album"": { ++ ""id"": 222, ++ ""title"": ""Time Out"", ++ ""artist"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ }, ++ ""image"": { ++ ""thumbnail"": ""https://example.com/time-out.jpg"" ++ } ++ } ++ }, ++ { ++ ""id"": 987654321, ++ ""title"": ""So What"", ++ ""duration"": 562, ++ ""track_number"": 2, ++ ""media_number"": 1, ++ ""performer"": { ++ ""id"": 333, ++ ""name"": ""Miles Davis"" ++ }, ++ ""album"": { ++ ""id"": 444, ++ ""title"": ""Kind of Blue"", ++ ""artist"": { ++ ""id"": 333, ++ ""name"": ""Miles Davis"" ++ }, ++ ""image"": { ++ ""thumbnail"": ""https://example.com/kind-of-blue.jpg"" ++ } ++ } ++ } ++ ] ++ } ++ }") ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.GetPlaylistTracksAsync("qobuz", "1578664"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Equal(2, result.Count); ++ ++ // First track ++ Assert.Equal("Take Five", result[0].Title); ++ Assert.Equal("Dave Brubeck Quartet", result[0].Artist); ++ Assert.Equal("My Jazz Playlist", result[0].Album); // Album should be playlist name ++ Assert.Equal(1, result[0].Track); // Track index starts at 1 ++ Assert.Equal("ext-qobuz-song-123456789", result[0].Id); ++ Assert.Equal("qobuz", result[0].ExternalProvider); ++ Assert.Equal("123456789", result[0].ExternalId); ++ ++ // Second track ++ Assert.Equal("So What", result[1].Title); ++ Assert.Equal("Miles Davis", result[1].Artist); ++ Assert.Equal("My Jazz Playlist", result[1].Album); // Album should be playlist name ++ Assert.Equal(2, result[1].Track); // Track index increments ++ Assert.Equal("ext-qobuz-song-987654321", result[1].Id); ++ } ++ ++ [Fact] ++ public async Task GetPlaylistTracksAsync_WithWrongProvider_ReturnsEmptyList() ++ { ++ // Act ++ var result = await _service.GetPlaylistTracksAsync("deezer", "12345"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Empty(result); ++ } ++ ++ [Fact] ++ public async Task GetPlaylistTracksAsync_WhenHttpFails_ReturnsEmptyList() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.NotFound ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.GetPlaylistTracksAsync("qobuz", "999999"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Empty(result); ++ } ++ ++ [Fact] ++ public async Task GetPlaylistTracksAsync_WithMissingPlaylistName_UsesDefaultName() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.OK, ++ Content = new StringContent(@"{ ++ ""id"": 1578664, ++ ""tracks"": { ++ ""items"": [ ++ { ++ ""id"": 123, ++ ""title"": ""Test Track"", ++ ""performer"": { ++ ""id"": 1, ++ ""name"": ""Test Artist"" ++ }, ++ ""album"": { ++ ""id"": 2, ++ ""title"": ""Test Album"", ++ ""artist"": { ++ ""id"": 1, ++ ""name"": ""Test Artist"" ++ } ++ } ++ } ++ ] ++ } ++ }") ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.GetPlaylistTracksAsync("qobuz", "1578664"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Single(result); ++ Assert.Equal("Unknown Playlist", result[0].Album); ++ } ++ ++ #endregion ++ ++ #region SearchSongsAsync Tests ++ ++ [Fact] ++ public async Task SearchSongsAsync_WithValidQuery_ReturnsSongs() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.OK, ++ Content = new StringContent(@"{ ++ ""tracks"": { ++ ""items"": [ ++ { ++ ""id"": 123456789, ++ ""title"": ""Take Five"", ++ ""duration"": 324, ++ ""track_number"": 1, ++ ""performer"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ }, ++ ""album"": { ++ ""id"": 222, ++ ""title"": ""Time Out"", ++ ""artist"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ } ++ } ++ } ++ ] ++ } ++ }") ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.SearchSongsAsync("Take Five", 20); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Single(result); ++ Assert.Equal("Take Five", result[0].Title); ++ Assert.Equal("Dave Brubeck Quartet", result[0].Artist); ++ } ++ ++ #endregion ++ ++ #region SearchAlbumsAsync Tests ++ ++ [Fact] ++ public async Task SearchAlbumsAsync_WithValidQuery_ReturnsAlbums() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.OK, ++ Content = new StringContent(@"{ ++ ""albums"": { ++ ""items"": [ ++ { ++ ""id"": 222, ++ ""title"": ""Time Out"", ++ ""tracks_count"": 7, ++ ""artist"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ }, ++ ""release_date_original"": ""1959-12-14"" ++ } ++ ] ++ } ++ }") ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.SearchAlbumsAsync("Time Out", 20); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Single(result); ++ Assert.Equal("Time Out", result[0].Title); ++ Assert.Equal("Dave Brubeck Quartet", result[0].Artist); ++ Assert.Equal(1959, result[0].Year); ++ } ++ ++ #endregion ++ ++ #region GetSongAsync Tests ++ ++ [Fact] ++ public async Task GetSongAsync_WithValidId_ReturnsSong() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.OK, ++ Content = new StringContent(@"{ ++ ""id"": 123456789, ++ ""title"": ""Take Five"", ++ ""duration"": 324, ++ ""track_number"": 1, ++ ""isrc"": ""USCO10300456"", ++ ""copyright"": ""(P) 1959 Columbia Records"", ++ ""performer"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ }, ++ ""composer"": { ++ ""id"": 999, ++ ""name"": ""Paul Desmond"" ++ }, ++ ""album"": { ++ ""id"": 222, ++ ""title"": ""Time Out"", ++ ""tracks_count"": 7, ++ ""release_date_original"": ""1959-12-14"", ++ ""artist"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ }, ++ ""genres_list"": [""Jazz"", ""Jazz→Cool Jazz""] ++ } ++ }") ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.GetSongAsync("qobuz", "123456789"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Equal("Take Five", result.Title); ++ Assert.Equal("Dave Brubeck Quartet", result.Artist); ++ Assert.Equal("Time Out", result.Album); ++ Assert.Equal("USCO10300456", result.Isrc); ++ Assert.Equal("℗ 1959 Columbia Records", result.Copyright); ++ Assert.Equal(1959, result.Year); ++ Assert.Equal("1959-12-14", result.ReleaseDate); ++ Assert.Contains("Paul Desmond", result.Contributors); ++ Assert.Equal("Jazz, Cool Jazz", result.Genre); ++ } ++ ++ [Fact] ++ public async Task GetSongAsync_WithWrongProvider_ReturnsNull() ++ { ++ // Act ++ var result = await _service.GetSongAsync("deezer", "123456789"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ #endregion ++ ++ #region GetAlbumAsync Tests ++ ++ [Fact] ++ public async Task GetAlbumAsync_WithValidId_ReturnsAlbumWithTracks() ++ { ++ // Arrange ++ var mockResponse = new HttpResponseMessage ++ { ++ StatusCode = HttpStatusCode.OK, ++ Content = new StringContent(@"{ ++ ""id"": 222, ++ ""title"": ""Time Out"", ++ ""tracks_count"": 2, ++ ""release_date_original"": ""1959-12-14"", ++ ""artist"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ }, ++ ""genres_list"": [""Jazz""], ++ ""tracks"": { ++ ""items"": [ ++ { ++ ""id"": 1, ++ ""title"": ""Blue Rondo à la Turk"", ++ ""track_number"": 1, ++ ""performer"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ }, ++ ""album"": { ++ ""id"": 222, ++ ""title"": ""Time Out"", ++ ""artist"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ } ++ } ++ }, ++ { ++ ""id"": 2, ++ ""title"": ""Take Five"", ++ ""track_number"": 2, ++ ""performer"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ }, ++ ""album"": { ++ ""id"": 222, ++ ""title"": ""Time Out"", ++ ""artist"": { ++ ""id"": 111, ++ ""name"": ""Dave Brubeck Quartet"" ++ } ++ } ++ } ++ ] ++ } ++ }") ++ }; ++ ++ _httpMessageHandlerMock.Protected() ++ .Setup>( ++ "SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(mockResponse); ++ ++ // Act ++ var result = await _service.GetAlbumAsync("qobuz", "222"); ++ ++ // Assert ++ Assert.NotNull(result); ++ Assert.Equal("Time Out", result.Title); ++ Assert.Equal("Dave Brubeck Quartet", result.Artist); ++ Assert.Equal(1959, result.Year); ++ Assert.Equal(2, result.Songs.Count); ++ Assert.Equal("Blue Rondo à la Turk", result.Songs[0].Title); ++ Assert.Equal("Take Five", result.Songs[1].Title); ++ } ++ ++ [Fact] ++ public async Task GetAlbumAsync_WithWrongProvider_ReturnsNull() ++ { ++ // Act ++ var result = await _service.GetAlbumAsync("deezer", "222"); ++ ++ // Assert ++ Assert.Null(result); ++ } ++ ++ #endregion ++} +diff --git a/allstarr.Tests/SubsonicModelMapperTests.cs b/allstarr.Tests/SubsonicModelMapperTests.cs +new file mode 100644 +index 0000000..f3d39ee +--- /dev/null ++++ b/allstarr.Tests/SubsonicModelMapperTests.cs +@@ -0,0 +1,321 @@ ++using Microsoft.Extensions.Logging; ++using Moq; ++using allstarr.Models.Domain; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using allstarr.Services.Subsonic; ++using System.Text; ++using System.Text.Json; ++using System.Xml.Linq; ++ ++namespace allstarr.Tests; ++ ++public class SubsonicModelMapperTests ++{ ++ private readonly SubsonicModelMapper _mapper; ++ private readonly Mock> _mockLogger; ++ private readonly SubsonicResponseBuilder _responseBuilder; ++ ++ public SubsonicModelMapperTests() ++ { ++ _responseBuilder = new SubsonicResponseBuilder(); ++ _mockLogger = new Mock>(); ++ _mapper = new SubsonicModelMapper(_responseBuilder, _mockLogger.Object); ++ } ++ ++ [Fact] ++ public void ParseSearchResponse_JsonWithSongs_ParsesCorrectly() ++ { ++ // Arrange ++ var jsonResponse = @"{ ++ ""subsonic-response"": { ++ ""status"": ""ok"", ++ ""version"": ""1.16.1"", ++ ""searchResult3"": { ++ ""song"": [ ++ { ++ ""id"": ""song1"", ++ ""title"": ""Test Song"", ++ ""artist"": ""Test Artist"", ++ ""album"": ""Test Album"" ++ } ++ ] ++ } ++ } ++ }"; ++ var responseBody = Encoding.UTF8.GetBytes(jsonResponse); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseSearchResponse(responseBody, "application/json"); ++ ++ // Assert ++ Assert.Single(songs); ++ Assert.Empty(albums); ++ Assert.Empty(artists); ++ } ++ ++ [Fact] ++ public void ParseSearchResponse_XmlWithSongs_ParsesCorrectly() ++ { ++ // Arrange ++ var xmlResponse = @" ++ ++ ++ ++ ++"; ++ var responseBody = Encoding.UTF8.GetBytes(xmlResponse); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseSearchResponse(responseBody, "application/xml"); ++ ++ // Assert ++ Assert.Single(songs); ++ Assert.Empty(albums); ++ Assert.Empty(artists); ++ } ++ ++ [Fact] ++ public void ParseSearchResponse_JsonWithAllTypes_ParsesAllCorrectly() ++ { ++ // Arrange ++ var jsonResponse = @"{ ++ ""subsonic-response"": { ++ ""status"": ""ok"", ++ ""version"": ""1.16.1"", ++ ""searchResult3"": { ++ ""song"": [ ++ {""id"": ""song1"", ""title"": ""Song 1""} ++ ], ++ ""album"": [ ++ {""id"": ""album1"", ""name"": ""Album 1""} ++ ], ++ ""artist"": [ ++ {""id"": ""artist1"", ""name"": ""Artist 1""} ++ ] ++ } ++ } ++ }"; ++ var responseBody = Encoding.UTF8.GetBytes(jsonResponse); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseSearchResponse(responseBody, "application/json"); ++ ++ // Assert ++ Assert.Single(songs); ++ Assert.Single(albums); ++ Assert.Single(artists); ++ } ++ ++ [Fact] ++ public void ParseSearchResponse_XmlWithAllTypes_ParsesAllCorrectly() ++ { ++ // Arrange ++ var xmlResponse = @" ++ ++ ++ ++ ++ ++ ++"; ++ var responseBody = Encoding.UTF8.GetBytes(xmlResponse); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseSearchResponse(responseBody, "application/xml"); ++ ++ // Assert ++ Assert.Single(songs); ++ Assert.Single(albums); ++ Assert.Single(artists); ++ } ++ ++ [Fact] ++ public void ParseSearchResponse_InvalidJson_ReturnsEmpty() ++ { ++ // Arrange ++ var invalidJson = "{invalid json}"; ++ var responseBody = Encoding.UTF8.GetBytes(invalidJson); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseSearchResponse(responseBody, "application/json"); ++ ++ // Assert ++ Assert.Empty(songs); ++ Assert.Empty(albums); ++ Assert.Empty(artists); ++ } ++ ++ [Fact] ++ public void ParseSearchResponse_EmptySearchResult_ReturnsEmpty() ++ { ++ // Arrange ++ var jsonResponse = @"{ ++ ""subsonic-response"": { ++ ""status"": ""ok"", ++ ""version"": ""1.16.1"", ++ ""searchResult3"": {} ++ } ++ }"; ++ var responseBody = Encoding.UTF8.GetBytes(jsonResponse); ++ ++ // Act ++ var (songs, albums, artists) = _mapper.ParseSearchResponse(responseBody, "application/json"); ++ ++ // Assert ++ Assert.Empty(songs); ++ Assert.Empty(albums); ++ Assert.Empty(artists); ++ } ++ ++ [Fact] ++ public void MergeSearchResults_Json_MergesSongsCorrectly() ++ { ++ // Arrange ++ var localSongs = new List ++ { ++ new Dictionary { ["id"] = "local1", ["title"] = "Local Song" } ++ }; ++ var externalResult = new SearchResult ++ { ++ Songs = new List ++ { ++ new Song { Id = "ext1", Title = "External Song" } ++ }, ++ Albums = new List(), ++ Artists = new List() ++ }; ++ ++ // Act ++ var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults( ++ localSongs, new List(), new List(), externalResult, new List(), true); ++ ++ // Assert ++ Assert.Equal(2, mergedSongs.Count); ++ } ++ ++ [Fact] ++ public void MergeSearchResults_Json_CaseInsensitiveDeduplication() ++ { ++ // Arrange ++ var localArtists = new List ++ { ++ new Dictionary { ["id"] = "local1", ["name"] = "Test Artist" } ++ }; ++ var externalResult = new SearchResult ++ { ++ Songs = new List(), ++ Albums = new List(), ++ Artists = new List ++ { ++ new Artist { Id = "ext1", Name = "test artist" } // Different case - should still be filtered ++ } ++ }; ++ ++ // Act ++ var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults( ++ new List(), new List(), localArtists, externalResult, new List(), true); ++ ++ // Assert ++ Assert.Single(mergedArtists); // Only the local artist ++ } ++ ++ [Fact] ++ public void MergeSearchResults_Xml_MergesSongsCorrectly() ++ { ++ // Arrange ++ var ns = XNamespace.Get("http://subsonic.org/restapi"); ++ var localSongs = new List ++ { ++ new XElement("song", new XAttribute("id", "local1"), new XAttribute("title", "Local Song")) ++ }; ++ var externalResult = new SearchResult ++ { ++ Songs = new List ++ { ++ new Song { Id = "ext1", Title = "External Song" } ++ }, ++ Albums = new List(), ++ Artists = new List() ++ }; ++ ++ // Act ++ var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults( ++ localSongs, new List(), new List(), externalResult, new List(), false); ++ ++ // Assert ++ Assert.Equal(2, mergedSongs.Count); ++ } ++ ++ [Fact] ++ public void MergeSearchResults_Xml_DeduplicatesArtists() ++ { ++ // Arrange ++ var localArtists = new List ++ { ++ new XElement("artist", new XAttribute("id", "local1"), new XAttribute("name", "Test Artist")) ++ }; ++ var externalResult = new SearchResult ++ { ++ Songs = new List(), ++ Albums = new List(), ++ Artists = new List ++ { ++ new Artist { Id = "ext1", Name = "Test Artist" }, // Same name - should be filtered ++ new Artist { Id = "ext2", Name = "Different Artist" } // Different name - should be included ++ } ++ }; ++ ++ // Act ++ var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults( ++ new List(), new List(), localArtists, externalResult, new List(), false); ++ ++ // Assert ++ Assert.Equal(2, mergedArtists.Count); // 1 local + 1 external (duplicate filtered) ++ } ++ ++ [Fact] ++ public void MergeSearchResults_EmptyLocalResults_ReturnsOnlyExternal() ++ { ++ // Arrange ++ var externalResult = new SearchResult ++ { ++ Songs = new List { new Song { Id = "ext1" } }, ++ Albums = new List { new Album { Id = "ext2" } }, ++ Artists = new List { new Artist { Id = "ext3", Name = "Artist" } } ++ }; ++ ++ // Act ++ var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults( ++ new List(), new List(), new List(), externalResult, new List(), true); ++ ++ // Assert ++ Assert.Single(mergedSongs); ++ Assert.Single(mergedAlbums); ++ Assert.Single(mergedArtists); ++ } ++ ++ [Fact] ++ public void MergeSearchResults_EmptyExternalResults_ReturnsOnlyLocal() ++ { ++ // Arrange ++ var localSongs = new List { new Dictionary { ["id"] = "local1" } }; ++ var localAlbums = new List { new Dictionary { ["id"] = "local2" } }; ++ var localArtists = new List { new Dictionary { ["id"] = "local3", ["name"] = "Local" } }; ++ var externalResult = new SearchResult ++ { ++ Songs = new List(), ++ Albums = new List(), ++ Artists = new List() ++ }; ++ ++ // Act ++ var (mergedSongs, mergedAlbums, mergedArtists) = _mapper.MergeSearchResults( ++ localSongs, localAlbums, localArtists, externalResult, new List(), true); ++ ++ // Assert ++ Assert.Single(mergedSongs); ++ Assert.Single(mergedAlbums); ++ Assert.Single(mergedArtists); ++ } ++} +diff --git a/allstarr.Tests/SubsonicProxyServiceTests.cs b/allstarr.Tests/SubsonicProxyServiceTests.cs +new file mode 100644 +index 0000000..fbc9cd8 +--- /dev/null ++++ b/allstarr.Tests/SubsonicProxyServiceTests.cs +@@ -0,0 +1,423 @@ ++using Microsoft.AspNetCore.Mvc; ++using Microsoft.Extensions.Options; ++using Microsoft.AspNetCore.Http; ++using Moq; ++using Moq.Protected; ++using allstarr.Models.Settings; ++using allstarr.Services.Subsonic; ++using System.Net; ++ ++namespace allstarr.Tests; ++ ++public class SubsonicProxyServiceTests ++{ ++ private readonly SubsonicProxyService _service; ++ private readonly Mock _mockHttpMessageHandler; ++ private readonly Mock _mockHttpClientFactory; ++ ++ public SubsonicProxyServiceTests() ++ { ++ _mockHttpMessageHandler = new Mock(); ++ var httpClient = new HttpClient(_mockHttpMessageHandler.Object); ++ ++ _mockHttpClientFactory = new Mock(); ++ _mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(httpClient); ++ ++ var settings = Options.Create(new SubsonicSettings ++ { ++ Url = "http://localhost:4533" ++ }); ++ ++ var httpContext = new DefaultHttpContext(); ++ var httpContextAccessor = new HttpContextAccessor ++ { ++ HttpContext = httpContext ++ }; ++ ++ _service = new SubsonicProxyService(_mockHttpClientFactory.Object, settings, httpContextAccessor); ++ } ++ ++ [Fact] ++ public async Task RelayAsync_SuccessfulRequest_ReturnsBodyAndContentType() ++ { ++ // Arrange ++ var responseContent = new byte[] { 1, 2, 3, 4, 5 }; ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new ByteArrayContent(responseContent) ++ }; ++ responseMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(responseMessage); ++ ++ var parameters = new Dictionary ++ { ++ { "u", "admin" }, ++ { "p", "password" }, ++ { "v", "1.16.0" } ++ }; ++ ++ // Act ++ var (body, contentType) = await _service.RelayAsync("rest/ping", parameters); ++ ++ // Assert ++ Assert.Equal(responseContent, body); ++ Assert.Equal("application/json", contentType); ++ } ++ ++ [Fact] ++ public async Task RelayAsync_BuildsCorrectUrl() ++ { ++ // Arrange ++ HttpRequestMessage? capturedRequest = null; ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new ByteArrayContent(Array.Empty()) ++ }; ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .Callback((req, ct) => capturedRequest = req) ++ .ReturnsAsync(responseMessage); ++ ++ var parameters = new Dictionary ++ { ++ { "u", "admin" }, ++ { "p", "secret" } ++ }; ++ ++ // Act ++ await _service.RelayAsync("rest/ping", parameters); ++ ++ // Assert ++ Assert.NotNull(capturedRequest); ++ Assert.Contains("http://localhost:4533/rest/ping", capturedRequest!.RequestUri!.ToString()); ++ Assert.Contains("u=admin", capturedRequest.RequestUri.ToString()); ++ Assert.Contains("p=secret", capturedRequest.RequestUri.ToString()); ++ } ++ ++ [Fact] ++ public async Task RelayAsync_EncodesSpecialCharacters() ++ { ++ // Arrange ++ HttpRequestMessage? capturedRequest = null; ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new ByteArrayContent(Array.Empty()) ++ }; ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .Callback((req, ct) => capturedRequest = req) ++ .ReturnsAsync(responseMessage); ++ ++ var parameters = new Dictionary ++ { ++ { "query", "rock & roll" }, ++ { "artist", "AC/DC" } ++ }; ++ ++ // Act ++ await _service.RelayAsync("rest/search3", parameters); ++ ++ // Assert ++ Assert.NotNull(capturedRequest); ++ var url = capturedRequest!.RequestUri!.ToString(); ++ // HttpClient automatically applies URL encoding when building the URI ++ // Space can be encoded as + or %20, & as %26, / as %2F ++ Assert.Contains("query=", url); ++ Assert.Contains("artist=", url); ++ Assert.Contains("AC%2FDC", url); // / should be encoded as %2F ++ } ++ ++ [Fact] ++ public async Task RelayAsync_HttpError_ThrowsException() ++ { ++ // Arrange ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.NotFound); ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(responseMessage); ++ ++ var parameters = new Dictionary { { "u", "admin" } }; ++ ++ // Act & Assert ++ await Assert.ThrowsAsync(() => ++ _service.RelayAsync("rest/ping", parameters)); ++ } ++ ++ [Fact] ++ public async Task RelaySafeAsync_SuccessfulRequest_ReturnsSuccessTrue() ++ { ++ // Arrange ++ var responseContent = new byte[] { 1, 2, 3 }; ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new ByteArrayContent(responseContent) ++ }; ++ responseMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/xml"); ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(responseMessage); ++ ++ var parameters = new Dictionary { { "u", "admin" } }; ++ ++ // Act ++ var (body, contentType, success) = await _service.RelaySafeAsync("rest/ping", parameters); ++ ++ // Assert ++ Assert.True(success); ++ Assert.Equal(responseContent, body); ++ Assert.Equal("application/xml", contentType); ++ } ++ ++ [Fact] ++ public async Task RelaySafeAsync_HttpError_ReturnsSuccessFalse() ++ { ++ // Arrange ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.InternalServerError); ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(responseMessage); ++ ++ var parameters = new Dictionary { { "u", "admin" } }; ++ ++ // Act ++ var (body, contentType, success) = await _service.RelaySafeAsync("rest/ping", parameters); ++ ++ // Assert ++ Assert.False(success); ++ Assert.Null(body); ++ Assert.Null(contentType); ++ } ++ ++ [Fact] ++ public async Task RelaySafeAsync_NetworkException_ReturnsSuccessFalse() ++ { ++ // Arrange ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ThrowsAsync(new HttpRequestException("Network error")); ++ ++ var parameters = new Dictionary { { "u", "admin" } }; ++ ++ // Act ++ var (body, contentType, success) = await _service.RelaySafeAsync("rest/ping", parameters); ++ ++ // Assert ++ Assert.False(success); ++ Assert.Null(body); ++ Assert.Null(contentType); ++ } ++ ++ [Fact] ++ public async Task RelayStreamAsync_SuccessfulRequest_ReturnsFileStreamResult() ++ { ++ // Arrange ++ var streamContent = new byte[] { 1, 2, 3, 4, 5 }; ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new ByteArrayContent(streamContent) ++ }; ++ responseMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("audio/mpeg"); ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(responseMessage); ++ ++ var parameters = new Dictionary ++ { ++ { "id", "song123" }, ++ { "u", "admin" } ++ }; ++ ++ // Act ++ var result = await _service.RelayStreamAsync(parameters, CancellationToken.None); ++ ++ // Assert ++ var fileResult = Assert.IsType(result); ++ Assert.Equal("audio/mpeg", fileResult.ContentType); ++ Assert.True(fileResult.EnableRangeProcessing); ++ } ++ ++ [Fact] ++ public async Task RelayStreamAsync_HttpError_ReturnsStatusCodeResult() ++ { ++ // Arrange ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.NotFound); ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(responseMessage); ++ ++ var parameters = new Dictionary { { "id", "song123" } }; ++ ++ // Act ++ var result = await _service.RelayStreamAsync(parameters, CancellationToken.None); ++ ++ // Assert ++ var statusResult = Assert.IsType(result); ++ Assert.Equal(404, statusResult.StatusCode); ++ } ++ ++ [Fact] ++ public async Task RelayStreamAsync_Exception_ReturnsObjectResultWith500() ++ { ++ // Arrange ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ThrowsAsync(new HttpRequestException("Connection failed")); ++ ++ var parameters = new Dictionary { { "id", "song123" } }; ++ ++ // Act ++ var result = await _service.RelayStreamAsync(parameters, CancellationToken.None); ++ ++ // Assert ++ var objectResult = Assert.IsType(result); ++ Assert.Equal(500, objectResult.StatusCode); ++ } ++ ++ [Fact] ++ public async Task RelayStreamAsync_DefaultContentType_UsesAudioMpeg() ++ { ++ // Arrange ++ var streamContent = new byte[] { 1, 2, 3 }; ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new ByteArrayContent(streamContent) ++ // No ContentType set ++ }; ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .ReturnsAsync(responseMessage); ++ ++ var parameters = new Dictionary { { "id", "song123" } }; ++ ++ // Act ++ var result = await _service.RelayStreamAsync(parameters, CancellationToken.None); ++ ++ // Assert ++ var fileResult = Assert.IsType(result); ++ Assert.Equal("audio/mpeg", fileResult.ContentType); ++ } ++ ++ [Fact] ++ public async Task RelayStreamAsync_WithRangeHeader_ForwardsRangeToUpstream() ++ { ++ // Arrange ++ HttpRequestMessage? capturedRequest = null; ++ var streamContent = new byte[] { 1, 2, 3, 4, 5 }; ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.PartialContent) ++ { ++ Content = new ByteArrayContent(streamContent) ++ }; ++ responseMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("audio/mpeg"); ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .Callback((req, ct) => capturedRequest = req) ++ .ReturnsAsync(responseMessage); ++ ++ var httpContext = new DefaultHttpContext(); ++ httpContext.Request.Headers["Range"] = "bytes=0-1023"; ++ var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; ++ var service = new SubsonicProxyService(_mockHttpClientFactory.Object, ++ Options.Create(new SubsonicSettings { Url = "http://localhost:4533" }), ++ httpContextAccessor); ++ ++ var parameters = new Dictionary { { "id", "song123" } }; ++ ++ // Act ++ await service.RelayStreamAsync(parameters, CancellationToken.None); ++ ++ // Assert ++ Assert.NotNull(capturedRequest); ++ Assert.True(capturedRequest!.Headers.Contains("Range")); ++ Assert.Equal("bytes=0-1023", capturedRequest.Headers.GetValues("Range").First()); ++ } ++ ++ [Fact] ++ public async Task RelayStreamAsync_WithIfRangeHeader_ForwardsIfRangeToUpstream() ++ { ++ // Arrange ++ HttpRequestMessage? capturedRequest = null; ++ var streamContent = new byte[] { 1, 2, 3 }; ++ var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) ++ { ++ Content = new ByteArrayContent(streamContent) ++ }; ++ ++ _mockHttpMessageHandler.Protected() ++ .Setup>("SendAsync", ++ ItExpr.IsAny(), ++ ItExpr.IsAny()) ++ .Callback((req, ct) => capturedRequest = req) ++ .ReturnsAsync(responseMessage); ++ ++ var httpContext = new DefaultHttpContext(); ++ httpContext.Request.Headers["If-Range"] = "\"etag123\""; ++ var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; ++ var service = new SubsonicProxyService(_mockHttpClientFactory.Object, ++ Options.Create(new SubsonicSettings { Url = "http://localhost:4533" }), ++ httpContextAccessor); ++ ++ var parameters = new Dictionary { { "id", "song123" } }; ++ ++ // Act ++ await service.RelayStreamAsync(parameters, CancellationToken.None); ++ ++ // Assert ++ Assert.NotNull(capturedRequest); ++ Assert.True(capturedRequest!.Headers.Contains("If-Range")); ++ } ++ ++ [Fact] ++ public async Task RelayStreamAsync_NullHttpContext_ReturnsError() ++ { ++ // Arrange ++ var httpContextAccessor = new HttpContextAccessor { HttpContext = null }; ++ var service = new SubsonicProxyService(_mockHttpClientFactory.Object, ++ Options.Create(new SubsonicSettings { Url = "http://localhost:4533" }), ++ httpContextAccessor); ++ ++ var parameters = new Dictionary { { "id", "song123" } }; ++ ++ // Act ++ var result = await service.RelayStreamAsync(parameters, CancellationToken.None); ++ ++ // Assert ++ var objectResult = Assert.IsType(result); ++ Assert.Equal(500, objectResult.StatusCode); ++ } ++} +diff --git a/allstarr.Tests/SubsonicRequestParserTests.cs b/allstarr.Tests/SubsonicRequestParserTests.cs +new file mode 100644 +index 0000000..7fe76a0 +--- /dev/null ++++ b/allstarr.Tests/SubsonicRequestParserTests.cs +@@ -0,0 +1,202 @@ ++using Microsoft.AspNetCore.Http; ++using allstarr.Services.Subsonic; ++using System.Text; ++ ++namespace allstarr.Tests; ++ ++public class SubsonicRequestParserTests ++{ ++ private readonly SubsonicRequestParser _parser; ++ ++ public SubsonicRequestParserTests() ++ { ++ _parser = new SubsonicRequestParser(); ++ } ++ ++ [Fact] ++ public async Task ExtractAllParametersAsync_QueryParameters_ExtractsCorrectly() ++ { ++ // Arrange ++ var context = new DefaultHttpContext(); ++ context.Request.QueryString = new QueryString("?u=admin&p=password&v=1.16.0&c=testclient&f=json"); ++ ++ // Act ++ var result = await _parser.ExtractAllParametersAsync(context.Request); ++ ++ // Assert ++ Assert.Equal(5, result.Count); ++ Assert.Equal("admin", result["u"]); ++ Assert.Equal("password", result["p"]); ++ Assert.Equal("1.16.0", result["v"]); ++ Assert.Equal("testclient", result["c"]); ++ Assert.Equal("json", result["f"]); ++ } ++ ++ [Fact] ++ public async Task ExtractAllParametersAsync_FormEncodedBody_ExtractsCorrectly() ++ { ++ // Arrange ++ var context = new DefaultHttpContext(); ++ var formData = "u=admin&p=password&query=test+artist&artistCount=10"; ++ var bytes = Encoding.UTF8.GetBytes(formData); ++ ++ context.Request.Body = new MemoryStream(bytes); ++ context.Request.ContentType = "application/x-www-form-urlencoded"; ++ context.Request.ContentLength = bytes.Length; ++ context.Request.Method = "POST"; ++ ++ // Act ++ var result = await _parser.ExtractAllParametersAsync(context.Request); ++ ++ // Assert ++ Assert.Equal(4, result.Count); ++ Assert.Equal("admin", result["u"]); ++ Assert.Equal("password", result["p"]); ++ Assert.Equal("test artist", result["query"]); ++ Assert.Equal("10", result["artistCount"]); ++ } ++ ++ [Fact] ++ public async Task ExtractAllParametersAsync_JsonBody_ExtractsCorrectly() ++ { ++ // Arrange ++ var context = new DefaultHttpContext(); ++ var jsonData = "{\"u\":\"admin\",\"p\":\"password\",\"query\":\"test artist\",\"artistCount\":10}"; ++ var bytes = Encoding.UTF8.GetBytes(jsonData); ++ ++ context.Request.Body = new MemoryStream(bytes); ++ context.Request.ContentType = "application/json"; ++ context.Request.ContentLength = bytes.Length; ++ ++ // Act ++ var result = await _parser.ExtractAllParametersAsync(context.Request); ++ ++ // Assert ++ Assert.Equal(4, result.Count); ++ Assert.Equal("admin", result["u"]); ++ Assert.Equal("password", result["p"]); ++ Assert.Equal("test artist", result["query"]); ++ Assert.Equal("10", result["artistCount"]); ++ } ++ ++ [Fact] ++ public async Task ExtractAllParametersAsync_QueryAndFormBody_MergesCorrectly() ++ { ++ // Arrange ++ var context = new DefaultHttpContext(); ++ context.Request.QueryString = new QueryString("?u=admin&p=password&f=json"); ++ ++ var formData = "query=test&artistCount=5"; ++ var bytes = Encoding.UTF8.GetBytes(formData); ++ context.Request.Body = new MemoryStream(bytes); ++ context.Request.ContentType = "application/x-www-form-urlencoded"; ++ context.Request.ContentLength = bytes.Length; ++ context.Request.Method = "POST"; ++ ++ // Act ++ var result = await _parser.ExtractAllParametersAsync(context.Request); ++ ++ // Assert ++ Assert.Equal(5, result.Count); ++ Assert.Equal("admin", result["u"]); ++ Assert.Equal("password", result["p"]); ++ Assert.Equal("json", result["f"]); ++ Assert.Equal("test", result["query"]); ++ Assert.Equal("5", result["artistCount"]); ++ } ++ ++ [Fact] ++ public async Task ExtractAllParametersAsync_EmptyRequest_ReturnsEmptyDictionary() ++ { ++ // Arrange ++ var context = new DefaultHttpContext(); ++ ++ // Act ++ var result = await _parser.ExtractAllParametersAsync(context.Request); ++ ++ // Assert ++ Assert.Empty(result); ++ } ++ ++ [Fact] ++ public async Task ExtractAllParametersAsync_SpecialCharacters_EncodesCorrectly() ++ { ++ // Arrange ++ var context = new DefaultHttpContext(); ++ context.Request.QueryString = new QueryString("?query=rock+%26+roll&artist=AC%2FDC"); ++ ++ // Act ++ var result = await _parser.ExtractAllParametersAsync(context.Request); ++ ++ // Assert ++ Assert.Equal(2, result.Count); ++ Assert.Equal("rock & roll", result["query"]); ++ Assert.Equal("AC/DC", result["artist"]); ++ } ++ ++ [Fact] ++ public async Task ExtractAllParametersAsync_InvalidJson_IgnoresBody() ++ { ++ // Arrange ++ var context = new DefaultHttpContext(); ++ context.Request.QueryString = new QueryString("?u=admin"); ++ ++ var invalidJson = "{invalid json}"; ++ var bytes = Encoding.UTF8.GetBytes(invalidJson); ++ context.Request.Body = new MemoryStream(bytes); ++ context.Request.ContentType = "application/json"; ++ context.Request.ContentLength = bytes.Length; ++ ++ // Act ++ var result = await _parser.ExtractAllParametersAsync(context.Request); ++ ++ // Assert ++ Assert.Single(result); ++ Assert.Equal("admin", result["u"]); ++ } ++ ++ [Fact] ++ public async Task ExtractAllParametersAsync_NullJsonValues_HandlesGracefully() ++ { ++ // Arrange ++ var context = new DefaultHttpContext(); ++ var jsonData = "{\"u\":\"admin\",\"p\":null,\"query\":\"test\"}"; ++ var bytes = Encoding.UTF8.GetBytes(jsonData); ++ ++ context.Request.Body = new MemoryStream(bytes); ++ context.Request.ContentType = "application/json"; ++ context.Request.ContentLength = bytes.Length; ++ ++ // Act ++ var result = await _parser.ExtractAllParametersAsync(context.Request); ++ ++ // Assert ++ Assert.Equal(3, result.Count); ++ Assert.Equal("admin", result["u"]); ++ Assert.Equal("", result["p"]); ++ Assert.Equal("test", result["query"]); ++ } ++ ++ [Fact] ++ public async Task ExtractAllParametersAsync_DuplicateKeys_BodyOverridesQuery() ++ { ++ // Arrange ++ var context = new DefaultHttpContext(); ++ context.Request.QueryString = new QueryString("?format=xml&query=old"); ++ ++ var jsonData = "{\"query\":\"new\",\"artist\":\"Beatles\"}"; ++ var bytes = Encoding.UTF8.GetBytes(jsonData); ++ context.Request.Body = new MemoryStream(bytes); ++ context.Request.ContentType = "application/json"; ++ context.Request.ContentLength = bytes.Length; ++ ++ // Act ++ var result = await _parser.ExtractAllParametersAsync(context.Request); ++ ++ // Assert ++ Assert.Equal(3, result.Count); ++ Assert.Equal("xml", result["format"]); ++ Assert.Equal("new", result["query"]); // Body overrides query ++ Assert.Equal("Beatles", result["artist"]); ++ } ++} +diff --git a/allstarr.Tests/SubsonicResponseBuilderTests.cs b/allstarr.Tests/SubsonicResponseBuilderTests.cs +new file mode 100644 +index 0000000..86105f3 +--- /dev/null ++++ b/allstarr.Tests/SubsonicResponseBuilderTests.cs +@@ -0,0 +1,322 @@ ++using Microsoft.AspNetCore.Mvc; ++using allstarr.Models.Domain; ++using allstarr.Services.Subsonic; ++using System.Text.Json; ++using System.Xml.Linq; ++ ++namespace allstarr.Tests; ++ ++public class SubsonicResponseBuilderTests ++{ ++ private readonly SubsonicResponseBuilder _builder; ++ ++ public SubsonicResponseBuilderTests() ++ { ++ _builder = new SubsonicResponseBuilder(); ++ } ++ ++ [Fact] ++ public void CreateResponse_JsonFormat_ReturnsJsonWithOkStatus() ++ { ++ // Act ++ var result = _builder.CreateResponse("json", "testElement", new { }); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ Assert.NotNull(jsonResult.Value); ++ ++ // Serialize and deserialize to check structure ++ var json = JsonSerializer.Serialize(jsonResult.Value); ++ var doc = JsonDocument.Parse(json); ++ Assert.Equal("ok", doc.RootElement.GetProperty("subsonic-response").GetProperty("status").GetString()); ++ Assert.Equal("1.16.1", doc.RootElement.GetProperty("subsonic-response").GetProperty("version").GetString()); ++ } ++ ++ [Fact] ++ public void CreateResponse_XmlFormat_ReturnsXmlWithOkStatus() ++ { ++ // Act ++ var result = _builder.CreateResponse("xml", "testElement", new { }); ++ ++ // Assert ++ var contentResult = Assert.IsType(result); ++ Assert.Equal("application/xml", contentResult.ContentType); ++ ++ var doc = XDocument.Parse(contentResult.Content!); ++ var root = doc.Root!; ++ Assert.Equal("subsonic-response", root.Name.LocalName); ++ Assert.Equal("ok", root.Attribute("status")?.Value); ++ Assert.Equal("1.16.1", root.Attribute("version")?.Value); ++ } ++ ++ [Fact] ++ public void CreateError_JsonFormat_ReturnsJsonWithError() ++ { ++ // Act ++ var result = _builder.CreateError("json", 70, "Test error message"); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ var json = JsonSerializer.Serialize(jsonResult.Value); ++ var doc = JsonDocument.Parse(json); ++ var response = doc.RootElement.GetProperty("subsonic-response"); ++ ++ Assert.Equal("failed", response.GetProperty("status").GetString()); ++ Assert.Equal(70, response.GetProperty("error").GetProperty("code").GetInt32()); ++ Assert.Equal("Test error message", response.GetProperty("error").GetProperty("message").GetString()); ++ } ++ ++ [Fact] ++ public void CreateError_XmlFormat_ReturnsXmlWithError() ++ { ++ // Act ++ var result = _builder.CreateError("xml", 70, "Test error message"); ++ ++ // Assert ++ var contentResult = Assert.IsType(result); ++ Assert.Equal("application/xml", contentResult.ContentType); ++ ++ var doc = XDocument.Parse(contentResult.Content!); ++ var root = doc.Root!; ++ Assert.Equal("failed", root.Attribute("status")?.Value); ++ ++ var ns = root.GetDefaultNamespace(); ++ var errorElement = root.Element(ns + "error"); ++ Assert.NotNull(errorElement); ++ Assert.Equal("70", errorElement.Attribute("code")?.Value); ++ Assert.Equal("Test error message", errorElement.Attribute("message")?.Value); ++ } ++ ++ [Fact] ++ public void CreateSongResponse_JsonFormat_ReturnsSongData() ++ { ++ // Arrange ++ var song = new Song ++ { ++ Id = "song123", ++ Title = "Test Song", ++ Artist = "Test Artist", ++ Album = "Test Album", ++ Duration = 180, ++ Track = 5, ++ Year = 2023, ++ Genre = "Rock", ++ LocalPath = "/music/test.mp3" ++ }; ++ ++ // Act ++ var result = _builder.CreateSongResponse("json", song); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ var json = JsonSerializer.Serialize(jsonResult.Value); ++ var doc = JsonDocument.Parse(json); ++ var songData = doc.RootElement.GetProperty("subsonic-response").GetProperty("song"); ++ ++ Assert.Equal("song123", songData.GetProperty("id").GetString()); ++ Assert.Equal("Test Song", songData.GetProperty("title").GetString()); ++ Assert.Equal("Test Artist", songData.GetProperty("artist").GetString()); ++ Assert.Equal("Test Album", songData.GetProperty("album").GetString()); ++ } ++ ++ [Fact] ++ public void CreateSongResponse_XmlFormat_ReturnsSongData() ++ { ++ // Arrange ++ var song = new Song ++ { ++ Id = "song123", ++ Title = "Test Song", ++ Artist = "Test Artist", ++ Album = "Test Album", ++ Duration = 180 ++ }; ++ ++ // Act ++ var result = _builder.CreateSongResponse("xml", song); ++ ++ // Assert ++ var contentResult = Assert.IsType(result); ++ Assert.Equal("application/xml", contentResult.ContentType); ++ ++ var doc = XDocument.Parse(contentResult.Content!); ++ var ns = doc.Root!.GetDefaultNamespace(); ++ var songElement = doc.Root!.Element(ns + "song"); ++ Assert.NotNull(songElement); ++ Assert.Equal("song123", songElement.Attribute("id")?.Value); ++ Assert.Equal("Test Song", songElement.Attribute("title")?.Value); ++ } ++ ++ [Fact] ++ public void CreateAlbumResponse_JsonFormat_ReturnsAlbumWithSongs() ++ { ++ // Arrange ++ var album = new Album ++ { ++ Id = "album123", ++ Title = "Test Album", ++ Artist = "Test Artist", ++ Year = 2023, ++ Songs = new List ++ { ++ new Song { Id = "song1", Title = "Song 1", Duration = 180 }, ++ new Song { Id = "song2", Title = "Song 2", Duration = 200 } ++ } ++ }; ++ ++ // Act ++ var result = _builder.CreateAlbumResponse("json", album); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ var json = JsonSerializer.Serialize(jsonResult.Value); ++ var doc = JsonDocument.Parse(json); ++ var albumData = doc.RootElement.GetProperty("subsonic-response").GetProperty("album"); ++ ++ Assert.Equal("album123", albumData.GetProperty("id").GetString()); ++ Assert.Equal("Test Album", albumData.GetProperty("name").GetString()); ++ Assert.Equal(2, albumData.GetProperty("songCount").GetInt32()); ++ Assert.Equal(380, albumData.GetProperty("duration").GetInt32()); ++ } ++ ++ [Fact] ++ public void CreateAlbumResponse_XmlFormat_ReturnsAlbumWithSongs() ++ { ++ // Arrange ++ var album = new Album ++ { ++ Id = "album123", ++ Title = "Test Album", ++ Artist = "Test Artist", ++ SongCount = 2, ++ Songs = new List ++ { ++ new Song { Id = "song1", Title = "Song 1" }, ++ new Song { Id = "song2", Title = "Song 2" } ++ } ++ }; ++ ++ // Act ++ var result = _builder.CreateAlbumResponse("xml", album); ++ ++ // Assert ++ var contentResult = Assert.IsType(result); ++ Assert.Equal("application/xml", contentResult.ContentType); ++ ++ var doc = XDocument.Parse(contentResult.Content!); ++ var ns = doc.Root!.GetDefaultNamespace(); ++ var albumElement = doc.Root!.Element(ns + "album"); ++ Assert.NotNull(albumElement); ++ Assert.Equal("album123", albumElement.Attribute("id")?.Value); ++ Assert.Equal("2", albumElement.Attribute("songCount")?.Value); ++ } ++ ++ [Fact] ++ public void CreateArtistResponse_JsonFormat_ReturnsArtistData() ++ { ++ // Arrange ++ var artist = new Artist ++ { ++ Id = "artist123", ++ Name = "Test Artist" ++ }; ++ var albums = new List ++ { ++ new Album { Id = "album1", Title = "Album 1" }, ++ new Album { Id = "album2", Title = "Album 2" } ++ }; ++ ++ // Act ++ var result = _builder.CreateArtistResponse("json", artist, albums); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ var json = JsonSerializer.Serialize(jsonResult.Value); ++ var doc = JsonDocument.Parse(json); ++ var artistData = doc.RootElement.GetProperty("subsonic-response").GetProperty("artist"); ++ ++ Assert.Equal("artist123", artistData.GetProperty("id").GetString()); ++ Assert.Equal("Test Artist", artistData.GetProperty("name").GetString()); ++ Assert.Equal(2, artistData.GetProperty("albumCount").GetInt32()); ++ } ++ ++ [Fact] ++ public void CreateArtistResponse_XmlFormat_ReturnsArtistData() ++ { ++ // Arrange ++ var artist = new Artist ++ { ++ Id = "artist123", ++ Name = "Test Artist" ++ }; ++ var albums = new List ++ { ++ new Album { Id = "album1", Title = "Album 1" }, ++ new Album { Id = "album2", Title = "Album 2" } ++ }; ++ ++ // Act ++ var result = _builder.CreateArtistResponse("xml", artist, albums); ++ ++ // Assert ++ var contentResult = Assert.IsType(result); ++ Assert.Equal("application/xml", contentResult.ContentType); ++ ++ var doc = XDocument.Parse(contentResult.Content!); ++ var ns = doc.Root!.GetDefaultNamespace(); ++ var artistElement = doc.Root!.Element(ns + "artist"); ++ Assert.NotNull(artistElement); ++ Assert.Equal("artist123", artistElement.Attribute("id")?.Value); ++ Assert.Equal("Test Artist", artistElement.Attribute("name")?.Value); ++ Assert.Equal("2", artistElement.Attribute("albumCount")?.Value); ++ } ++ ++ [Fact] ++ public void CreateSongResponse_SongWithNullValues_HandlesGracefully() ++ { ++ // Arrange ++ var song = new Song ++ { ++ Id = "song123", ++ Title = "Test Song" ++ // Other fields are null ++ }; ++ ++ // Act ++ var result = _builder.CreateSongResponse("json", song); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ var json = JsonSerializer.Serialize(jsonResult.Value); ++ var doc = JsonDocument.Parse(json); ++ var songData = doc.RootElement.GetProperty("subsonic-response").GetProperty("song"); ++ ++ Assert.Equal("song123", songData.GetProperty("id").GetString()); ++ Assert.Equal("Test Song", songData.GetProperty("title").GetString()); ++ } ++ ++ [Fact] ++ public void CreateAlbumResponse_EmptySongList_ReturnsZeroCounts() ++ { ++ // Arrange ++ var album = new Album ++ { ++ Id = "album123", ++ Title = "Empty Album", ++ Artist = "Test Artist", ++ Songs = new List() ++ }; ++ ++ // Act ++ var result = _builder.CreateAlbumResponse("json", album); ++ ++ // Assert ++ var jsonResult = Assert.IsType(result); ++ var json = JsonSerializer.Serialize(jsonResult.Value); ++ var doc = JsonDocument.Parse(json); ++ var albumData = doc.RootElement.GetProperty("subsonic-response").GetProperty("album"); ++ ++ Assert.Equal(0, albumData.GetProperty("songCount").GetInt32()); ++ Assert.Equal(0, albumData.GetProperty("duration").GetInt32()); ++ } ++} +diff --git a/allstarr.Tests/allstarr.Tests.csproj b/allstarr.Tests/allstarr.Tests.csproj +new file mode 100644 +index 0000000..bbb3a44 +--- /dev/null ++++ b/allstarr.Tests/allstarr.Tests.csproj +@@ -0,0 +1,28 @@ ++ ++ ++ ++ net9.0 ++ allstarr.Tests ++ enable ++ enable ++ false ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/allstarr.sln b/allstarr.sln +new file mode 100644 +index 0000000..b5273f3 +--- /dev/null ++++ b/allstarr.sln +@@ -0,0 +1,45 @@ ++ ++Microsoft Visual Studio Solution File, Format Version 12.00 ++Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "allstarr", "allstarr\allstarr.csproj", "{C56EF44B-3AD5-4D4C-A513-726DA8A0E225}" ++EndProject ++Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "allstarr.Tests", "allstarr.Tests\allstarr.Tests.csproj", "{72E3A16E-7020-4EE0-95D4-FB8FA027ED12}" ++EndProject ++Global ++ GlobalSection(SolutionConfigurationPlatforms) = preSolution ++ Debug|Any CPU = Debug|Any CPU ++ Debug|x64 = Debug|x64 ++ Debug|x86 = Debug|x86 ++ Release|Any CPU = Release|Any CPU ++ Release|x64 = Release|x64 ++ Release|x86 = Release|x86 ++ EndGlobalSection ++ GlobalSection(ProjectConfigurationPlatforms) = postSolution ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|Any CPU.Build.0 = Debug|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|x64.ActiveCfg = Debug|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|x64.Build.0 = Debug|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|x86.ActiveCfg = Debug|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Debug|x86.Build.0 = Debug|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|Any CPU.ActiveCfg = Release|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|Any CPU.Build.0 = Release|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|x64.ActiveCfg = Release|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|x64.Build.0 = Release|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|x86.ActiveCfg = Release|Any CPU ++ {C56EF44B-3AD5-4D4C-A513-726DA8A0E225}.Release|x86.Build.0 = Release|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|Any CPU.Build.0 = Debug|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|x64.ActiveCfg = Debug|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|x64.Build.0 = Debug|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|x86.ActiveCfg = Debug|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Debug|x86.Build.0 = Debug|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|Any CPU.ActiveCfg = Release|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|Any CPU.Build.0 = Release|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|x64.ActiveCfg = Release|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|x64.Build.0 = Release|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|x86.ActiveCfg = Release|Any CPU ++ {72E3A16E-7020-4EE0-95D4-FB8FA027ED12}.Release|x86.Build.0 = Release|Any CPU ++ EndGlobalSection ++ GlobalSection(SolutionProperties) = preSolution ++ HideSolutionNode = FALSE ++ EndGlobalSection ++EndGlobal +diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs +new file mode 100644 +index 0000000..5886dd4 +--- /dev/null ++++ b/allstarr/Controllers/JellyfinController.cs +@@ -0,0 +1,1642 @@ ++using Microsoft.AspNetCore.Mvc; ++using Microsoft.Extensions.Options; ++using System.Text.Json; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Subsonic; ++using allstarr.Services; ++using allstarr.Services.Common; ++using allstarr.Services.Local; ++using allstarr.Services.Jellyfin; ++using allstarr.Services.Subsonic; ++ ++namespace allstarr.Controllers; ++ ++/// ++/// Jellyfin-compatible API controller. Merges local library with external providers ++/// (Deezer, Qobuz, SquidWTF). Auth goes through Jellyfin. ++/// ++[ApiController] ++[Route("")] ++public class JellyfinController : ControllerBase ++{ ++ private readonly JellyfinSettings _settings; ++ private readonly IMusicMetadataService _metadataService; ++ private readonly ILocalLibraryService _localLibraryService; ++ private readonly IDownloadService _downloadService; ++ private readonly JellyfinResponseBuilder _responseBuilder; ++ private readonly JellyfinModelMapper _modelMapper; ++ private readonly JellyfinProxyService _proxyService; ++ private readonly PlaylistSyncService? _playlistSyncService; ++ private readonly ILogger _logger; ++ ++ public JellyfinController( ++ IOptions settings, ++ IMusicMetadataService metadataService, ++ ILocalLibraryService localLibraryService, ++ IDownloadService downloadService, ++ JellyfinResponseBuilder responseBuilder, ++ JellyfinModelMapper modelMapper, ++ JellyfinProxyService proxyService, ++ ILogger logger, ++ PlaylistSyncService? playlistSyncService = null) ++ { ++ _settings = settings.Value; ++ _metadataService = metadataService; ++ _localLibraryService = localLibraryService; ++ _downloadService = downloadService; ++ _responseBuilder = responseBuilder; ++ _modelMapper = modelMapper; ++ _proxyService = proxyService; ++ _playlistSyncService = playlistSyncService; ++ _logger = logger; ++ ++ if (string.IsNullOrWhiteSpace(_settings.Url)) ++ { ++ throw new InvalidOperationException("JELLYFIN_URL environment variable is not set"); ++ } ++ } ++ ++ #region Search ++ ++ /// ++ /// Searches local Jellyfin library and external providers. ++ /// Dedupes artists, combines songs/albums. Works with /Items and /Users/{userId}/Items. ++ /// ++ [HttpGet("Items", Order = 1)] ++ [HttpGet("Users/{userId}/Items", Order = 1)] ++ public async Task SearchItems( ++ [FromQuery] string? searchTerm, ++ [FromQuery] string? includeItemTypes, ++ [FromQuery] int limit = 20, ++ [FromQuery] int startIndex = 0, ++ [FromQuery] string? parentId = null, ++ [FromQuery] string? artistIds = null, ++ [FromQuery] string? sortBy = null, ++ [FromQuery] bool recursive = true, ++ string? userId = null) ++ { ++ _logger.LogInformation("=== SEARCHITEMS V2 CALLED === searchTerm={SearchTerm}, includeItemTypes={ItemTypes}, parentId={ParentId}, artistIds={ArtistIds}, userId={UserId}", ++ searchTerm, includeItemTypes, parentId, artistIds, userId); ++ ++ // If filtering by artist, handle external artists ++ if (!string.IsNullOrWhiteSpace(artistIds)) ++ { ++ var artistId = artistIds.Split(',')[0]; // Take first artist if multiple ++ var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(artistId); ++ ++ if (isExternal) ++ { ++ _logger.LogInformation("Fetching albums for external artist: {Provider}/{ExternalId}", provider, externalId); ++ return await GetExternalChildItems(provider!, externalId!, includeItemTypes); ++ } ++ } ++ ++ // If no search term, proxy to Jellyfin for browsing ++ // If Jellyfin returns empty results, we'll just return empty (not mixing browse with external) ++ if (string.IsNullOrWhiteSpace(searchTerm) && string.IsNullOrWhiteSpace(parentId)) ++ { ++ _logger.LogDebug("No search term or parentId, proxying to Jellyfin"); ++ var browseResult = await _proxyService.GetItemsAsync( ++ parentId: null, ++ includeItemTypes: ParseItemTypes(includeItemTypes), ++ sortBy: sortBy, ++ limit: limit, ++ startIndex: startIndex, ++ clientHeaders: Request.Headers); ++ ++ if (browseResult == null) ++ { ++ _logger.LogInformation("Jellyfin returned null, returning empty result"); ++ return new JsonResult(new Dictionary ++ { ++ ["Items"] = Array.Empty(), ++ ["TotalRecordCount"] = 0, ++ ["StartIndex"] = startIndex ++ }); ++ } ++ ++ var result = JsonSerializer.Deserialize(browseResult.RootElement.GetRawText()); ++ if (_logger.IsEnabled(LogLevel.Debug)) ++ { ++ var rawText = browseResult.RootElement.GetRawText(); ++ var preview = rawText.Length > 200 ? rawText[..200] : rawText; ++ _logger.LogDebug("Jellyfin browse result preview: {Result}", preview); ++ } ++ return new JsonResult(result); ++ } ++ ++ // If browsing a specific parent (album, artist, playlist) ++ if (!string.IsNullOrWhiteSpace(parentId)) ++ { ++ // Check if this is the music library root - if so, treat as a search ++ var isMusicLibrary = parentId == _settings.LibraryId; ++ ++ if (!isMusicLibrary || string.IsNullOrWhiteSpace(searchTerm)) ++ { ++ _logger.LogDebug("Browsing parent: {ParentId}", parentId); ++ return await GetChildItems(parentId, includeItemTypes, limit, startIndex, sortBy); ++ } ++ ++ // If searching within music library root, continue to integrated search below ++ _logger.LogInformation("Searching within music library {ParentId}, including external sources", parentId); ++ } ++ ++ var cleanQuery = searchTerm?.Trim().Trim('"') ?? ""; ++ _logger.LogInformation("Performing integrated search for: {Query}", cleanQuery); ++ ++ // Run local and external searches in parallel ++ var itemTypes = ParseItemTypes(includeItemTypes); ++ var jellyfinTask = _proxyService.SearchAsync(cleanQuery, itemTypes, limit, recursive, Request.Headers); ++ var externalTask = _metadataService.SearchAllAsync(cleanQuery, limit, limit, limit); ++ ++ var playlistTask = _settings.EnableExternalPlaylists ++ ? _metadataService.SearchPlaylistsAsync(cleanQuery, limit) ++ : Task.FromResult(new List()); ++ ++ await Task.WhenAll(jellyfinTask, externalTask, playlistTask); ++ ++ var jellyfinResult = await jellyfinTask; ++ var externalResult = await externalTask; ++ var playlistResult = await playlistTask; ++ ++ _logger.LogInformation("Search results: Jellyfin={JellyfinCount}, External Songs={ExtSongs}, Albums={ExtAlbums}, Artists={ExtArtists}, Playlists={Playlists}", ++ jellyfinResult != null ? "found" : "null", ++ externalResult.Songs.Count, ++ externalResult.Albums.Count, ++ externalResult.Artists.Count, ++ playlistResult.Count); ++ ++ // Parse Jellyfin results into domain models ++ var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult); ++ ++ // Score and filter Jellyfin results by relevance ++ var scoredLocalSongs = ScoreSearchResults(cleanQuery, localSongs, s => s.Title, s => s.Artist, isExternal: false); ++ var scoredLocalAlbums = ScoreSearchResults(cleanQuery, localAlbums, a => a.Title, a => a.Artist, isExternal: false); ++ var scoredLocalArtists = ScoreSearchResults(cleanQuery, localArtists, a => a.Name, _ => null, isExternal: false); ++ ++ // Score external results with a small boost ++ var scoredExternalSongs = ScoreSearchResults(cleanQuery, externalResult.Songs, s => s.Title, s => s.Artist, isExternal: true); ++ var scoredExternalAlbums = ScoreSearchResults(cleanQuery, externalResult.Albums, a => a.Title, a => a.Artist, isExternal: true); ++ var scoredExternalArtists = ScoreSearchResults(cleanQuery, externalResult.Artists, a => a.Name, _ => null, isExternal: true); ++ ++ // Merge and sort by score (only include items with score >= 40) ++ var allSongs = scoredLocalSongs.Concat(scoredExternalSongs) ++ .Where(x => x.Score >= 40) ++ .OrderByDescending(x => x.Score) ++ .Select(x => x.Item) ++ .ToList(); ++ ++ var allAlbums = scoredLocalAlbums.Concat(scoredExternalAlbums) ++ .Where(x => x.Score >= 40) ++ .OrderByDescending(x => x.Score) ++ .Select(x => x.Item) ++ .ToList(); ++ ++ // Dedupe artists by name, keeping highest scored version ++ var artistScores = scoredLocalArtists.Concat(scoredExternalArtists) ++ .Where(x => x.Score >= 40) ++ .GroupBy(x => x.Item.Name, StringComparer.OrdinalIgnoreCase) ++ .Select(g => g.OrderByDescending(x => x.Score).First()) ++ .OrderByDescending(x => x.Score) ++ .Select(x => x.Item) ++ .ToList(); ++ ++ // Convert to Jellyfin format ++ var mergedSongs = allSongs.Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)).ToList(); ++ var mergedAlbums = allAlbums.Select(a => _responseBuilder.ConvertAlbumToJellyfinItem(a)).ToList(); ++ var mergedArtists = artistScores.Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)).ToList(); ++ ++ // Add playlists (score them too) ++ if (playlistResult.Count > 0) ++ { ++ var scoredPlaylists = playlistResult ++ .Select(p => new { Playlist = p, Score = FuzzyMatcher.CalculateSimilarity(cleanQuery, p.Name) }) ++ .Where(x => x.Score >= 40) ++ .OrderByDescending(x => x.Score) ++ .Select(x => _responseBuilder.ConvertPlaylistToJellyfinItem(x.Playlist)) ++ .ToList(); ++ ++ mergedAlbums.AddRange(scoredPlaylists); ++ } ++ ++ _logger.LogInformation("Scored and filtered results: Songs={Songs}, Albums={Albums}, Artists={Artists}", ++ mergedSongs.Count, mergedAlbums.Count, mergedArtists.Count); ++ ++ // Filter by item types if specified ++ var items = new List>(); ++ ++ _logger.LogInformation("Filtering by item types: {ItemTypes}", itemTypes == null ? "null" : string.Join(",", itemTypes)); ++ ++ if (itemTypes == null || itemTypes.Length == 0 || itemTypes.Contains("MusicArtist")) ++ { ++ _logger.LogInformation("Adding {Count} artists to results", mergedArtists.Count); ++ items.AddRange(mergedArtists); ++ } ++ if (itemTypes == null || itemTypes.Length == 0 || itemTypes.Contains("MusicAlbum") || itemTypes.Contains("Playlist")) ++ { ++ _logger.LogInformation("Adding {Count} albums to results", mergedAlbums.Count); ++ items.AddRange(mergedAlbums); ++ } ++ if (itemTypes == null || itemTypes.Length == 0 || itemTypes.Contains("Audio")) ++ { ++ _logger.LogInformation("Adding {Count} songs to results", mergedSongs.Count); ++ items.AddRange(mergedSongs); ++ } ++ ++ // Apply pagination ++ var pagedItems = items.Skip(startIndex).Take(limit).ToList(); ++ ++ _logger.LogInformation("Returning {Count} items (total: {Total})", pagedItems.Count, items.Count); ++ ++ try ++ { ++ // Return with PascalCase - use ContentResult to bypass JSON serialization issues ++ var response = new ++ { ++ Items = pagedItems, ++ TotalRecordCount = items.Count, ++ StartIndex = startIndex ++ }; ++ ++ _logger.LogInformation("About to serialize response..."); ++ ++ var json = System.Text.Json.JsonSerializer.Serialize(response, new System.Text.Json.JsonSerializerOptions ++ { ++ PropertyNamingPolicy = null, ++ DictionaryKeyPolicy = null ++ }); ++ ++ if (_logger.IsEnabled(LogLevel.Debug)) ++ { ++ var preview = json.Length > 200 ? json[..200] : json; ++ _logger.LogDebug("JSON response preview: {Json}", preview); ++ } ++ ++ return Content(json, "application/json"); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error serializing search response"); ++ throw; ++ } ++ } ++ ++ /// ++ /// Gets child items of a parent (tracks in album, albums for artist). ++ /// ++ private async Task GetChildItems( ++ string parentId, ++ string? includeItemTypes, ++ int limit, ++ int startIndex, ++ string? sortBy) ++ { ++ // Check if this is an external playlist ++ if (PlaylistIdHelper.IsExternalPlaylist(parentId)) ++ { ++ return await GetPlaylistTracks(parentId); ++ } ++ ++ var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(parentId); ++ ++ if (isExternal) ++ { ++ // Get external album or artist content ++ return await GetExternalChildItems(provider!, externalId!, includeItemTypes); ++ } ++ ++ // Proxy to Jellyfin for local content ++ var result = await _proxyService.GetItemsAsync( ++ parentId: parentId, ++ includeItemTypes: ParseItemTypes(includeItemTypes), ++ sortBy: sortBy, ++ limit: limit, ++ startIndex: startIndex, ++ clientHeaders: Request.Headers); ++ ++ if (result == null) ++ { ++ return _responseBuilder.CreateError(404, "Parent not found"); ++ } ++ ++ return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); ++ } ++ ++ /// ++ /// Quick search endpoint. Works with /Search/Hints and /Users/{userId}/Search/Hints. ++ /// ++ [HttpGet("Search/Hints", Order = 1)] ++ [HttpGet("Users/{userId}/Search/Hints", Order = 1)] ++ public async Task SearchHints( ++ [FromQuery] string searchTerm, ++ [FromQuery] int limit = 20, ++ [FromQuery] string? includeItemTypes = null, ++ string? userId = null) ++ { ++ if (string.IsNullOrWhiteSpace(searchTerm)) ++ { ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ SearchHints = Array.Empty(), ++ TotalRecordCount = 0 ++ }); ++ } ++ ++ var cleanQuery = searchTerm.Trim().Trim('"'); ++ var itemTypes = ParseItemTypes(includeItemTypes); ++ ++ // Run searches in parallel ++ var jellyfinTask = _proxyService.SearchAsync(cleanQuery, itemTypes, limit, true, Request.Headers); ++ var externalTask = _metadataService.SearchAllAsync(cleanQuery, limit, limit, limit); ++ ++ await Task.WhenAll(jellyfinTask, externalTask); ++ ++ var jellyfinResult = await jellyfinTask; ++ var externalResult = await externalTask; ++ ++ var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult); ++ ++ // Merge and convert to search hints format ++ var allSongs = localSongs.Concat(externalResult.Songs).Take(limit).ToList(); ++ var allAlbums = localAlbums.Concat(externalResult.Albums).Take(limit).ToList(); ++ ++ // Dedupe artists by name ++ var artistNames = new HashSet(StringComparer.OrdinalIgnoreCase); ++ var allArtists = new List(); ++ foreach (var artist in localArtists.Concat(externalResult.Artists)) ++ { ++ if (artistNames.Add(artist.Name)) ++ { ++ allArtists.Add(artist); ++ } ++ } ++ ++ return _responseBuilder.CreateSearchHintsResponse( ++ allSongs.Take(limit).ToList(), ++ allAlbums.Take(limit).ToList(), ++ allArtists.Take(limit).ToList()); ++ } ++ ++ #endregion ++ ++ #region Items ++ ++ /// ++ /// Gets a single item by ID. ++ /// ++ [HttpGet("Items/{itemId}")] ++ [HttpGet("Users/{userId}/Items/{itemId}")] ++ public async Task GetItem(string itemId, string? userId = null) ++ { ++ if (string.IsNullOrWhiteSpace(itemId)) ++ { ++ return _responseBuilder.CreateError(400, "Missing item ID"); ++ } ++ ++ // Check for external playlist ++ if (PlaylistIdHelper.IsExternalPlaylist(itemId)) ++ { ++ return await GetPlaylistAsAlbum(itemId); ++ } ++ ++ var (isExternal, provider, type, externalId) = _localLibraryService.ParseExternalId(itemId); ++ ++ if (isExternal) ++ { ++ return await GetExternalItem(provider!, type, externalId!); ++ } ++ ++ // Proxy to Jellyfin ++ var result = await _proxyService.GetItemAsync(itemId, Request.Headers); ++ if (result == null) ++ { ++ return _responseBuilder.CreateError(404, "Item not found"); ++ } ++ ++ return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); ++ } ++ ++ /// ++ /// Gets an external item (song, album, or artist). ++ /// ++ private async Task GetExternalItem(string provider, string? type, string externalId) ++ { ++ switch (type) ++ { ++ case "song": ++ var song = await _metadataService.GetSongAsync(provider, externalId); ++ if (song == null) return _responseBuilder.CreateError(404, "Song not found"); ++ return _responseBuilder.CreateSongResponse(song); ++ ++ case "album": ++ var album = await _metadataService.GetAlbumAsync(provider, externalId); ++ if (album == null) return _responseBuilder.CreateError(404, "Album not found"); ++ return _responseBuilder.CreateAlbumResponse(album); ++ ++ case "artist": ++ var artist = await _metadataService.GetArtistAsync(provider, externalId); ++ if (artist == null) return _responseBuilder.CreateError(404, "Artist not found"); ++ var albums = await _metadataService.GetArtistAlbumsAsync(provider, externalId); ++ ++ // Fill in artist info for albums ++ foreach (var a in albums) ++ { ++ if (string.IsNullOrEmpty(a.Artist)) a.Artist = artist.Name; ++ if (string.IsNullOrEmpty(a.ArtistId)) a.ArtistId = artist.Id; ++ } ++ ++ return _responseBuilder.CreateArtistResponse(artist, albums); ++ ++ default: ++ // Try song first, then album ++ var s = await _metadataService.GetSongAsync(provider, externalId); ++ if (s != null) return _responseBuilder.CreateSongResponse(s); ++ ++ var alb = await _metadataService.GetAlbumAsync(provider, externalId); ++ if (alb != null) return _responseBuilder.CreateAlbumResponse(alb); ++ ++ return _responseBuilder.CreateError(404, "Item not found"); ++ } ++ } ++ ++ /// ++ /// Gets child items for an external parent (album tracks or artist albums). ++ /// ++ private async Task GetExternalChildItems(string provider, string externalId, string? includeItemTypes) ++ { ++ var itemTypes = ParseItemTypes(includeItemTypes); ++ ++ // Check if asking for audio (album tracks) ++ if (itemTypes?.Contains("Audio") == true) ++ { ++ var album = await _metadataService.GetAlbumAsync(provider, externalId); ++ if (album == null) ++ { ++ return _responseBuilder.CreateError(404, "Album not found"); ++ } ++ ++ return _responseBuilder.CreateItemsResponse(album.Songs); ++ } ++ ++ // Otherwise assume it's artist albums ++ var albums = await _metadataService.GetArtistAlbumsAsync(provider, externalId); ++ var artist = await _metadataService.GetArtistAsync(provider, externalId); ++ ++ // Fill artist info ++ if (artist != null) ++ { ++ foreach (var a in albums) ++ { ++ if (string.IsNullOrEmpty(a.Artist)) a.Artist = artist.Name; ++ if (string.IsNullOrEmpty(a.ArtistId)) a.ArtistId = artist.Id; ++ } ++ } ++ ++ return _responseBuilder.CreateAlbumsResponse(albums); ++ } ++ ++ #endregion ++ ++ #region Artists ++ ++ /// ++ /// Gets artists from the library. ++ /// Supports both /Artists and /Artists/AlbumArtists routes. ++ /// When searchTerm is provided, integrates external search results. ++ /// ++ [HttpGet("Artists", Order = 1)] ++ [HttpGet("Artists/AlbumArtists", Order = 1)] ++ public async Task GetArtists( ++ [FromQuery] string? searchTerm, ++ [FromQuery] int limit = 50, ++ [FromQuery] int startIndex = 0) ++ { ++ _logger.LogInformation("GetArtists called: searchTerm={SearchTerm}, limit={Limit}", searchTerm, limit); ++ ++ // If there's a search term, integrate external results ++ if (!string.IsNullOrWhiteSpace(searchTerm)) ++ { ++ var cleanQuery = searchTerm.Trim().Trim('"'); ++ _logger.LogInformation("Searching artists for: {Query}", cleanQuery); ++ ++ // Run local and external searches in parallel ++ var jellyfinTask = _proxyService.GetArtistsAsync(searchTerm, limit, startIndex, Request.Headers); ++ var externalTask = _metadataService.SearchArtistsAsync(cleanQuery, limit); ++ ++ await Task.WhenAll(jellyfinTask, externalTask); ++ ++ var jellyfinResult = await jellyfinTask; ++ var externalArtists = await externalTask; ++ ++ _logger.LogInformation("Artist search results: Jellyfin={JellyfinCount}, External={ExternalCount}", ++ jellyfinResult != null ? "found" : "null", externalArtists.Count); ++ ++ // Parse Jellyfin artists ++ var localArtists = new List(); ++ if (jellyfinResult != null && jellyfinResult.RootElement.TryGetProperty("Items", out var items)) ++ { ++ foreach (var item in items.EnumerateArray()) ++ { ++ localArtists.Add(_modelMapper.ParseArtist(item)); ++ } ++ } ++ ++ // Merge and deduplicate by name ++ var artistNames = new HashSet(StringComparer.OrdinalIgnoreCase); ++ var mergedArtists = new List(); ++ ++ foreach (var artist in localArtists) ++ { ++ if (artistNames.Add(artist.Name)) ++ { ++ mergedArtists.Add(artist); ++ } ++ } ++ ++ foreach (var artist in externalArtists) ++ { ++ if (artistNames.Add(artist.Name)) ++ { ++ mergedArtists.Add(artist); ++ } ++ } ++ ++ _logger.LogInformation("Returning {Count} merged artists", mergedArtists.Count); ++ ++ // Convert to Jellyfin format ++ var artistItems = mergedArtists.Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)).ToList(); ++ ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ Items = artistItems, ++ TotalRecordCount = artistItems.Count, ++ StartIndex = startIndex ++ }); ++ } ++ ++ // No search term - just proxy to Jellyfin ++ var result = await _proxyService.GetArtistsAsync(searchTerm, limit, startIndex, Request.Headers); ++ ++ if (result == null) ++ { ++ return new JsonResult(new Dictionary ++ { ++ ["Items"] = Array.Empty(), ++ ["TotalRecordCount"] = 0, ++ ["StartIndex"] = startIndex ++ }); ++ } ++ ++ return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); ++ } ++ ++ /// ++ /// Gets a single artist by ID or name. ++ /// This route has lower priority to avoid conflicting with Artists/AlbumArtists. ++ /// ++ [HttpGet("Artists/{artistIdOrName}", Order = 10)] ++ public async Task GetArtist(string artistIdOrName) ++ { ++ var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(artistIdOrName); ++ ++ if (isExternal) ++ { ++ var artist = await _metadataService.GetArtistAsync(provider!, externalId!); ++ if (artist == null) ++ { ++ return _responseBuilder.CreateError(404, "Artist not found"); ++ } ++ ++ var albums = await _metadataService.GetArtistAlbumsAsync(provider!, externalId!); ++ foreach (var a in albums) ++ { ++ if (string.IsNullOrEmpty(a.Artist)) a.Artist = artist.Name; ++ if (string.IsNullOrEmpty(a.ArtistId)) a.ArtistId = artist.Id; ++ } ++ ++ return _responseBuilder.CreateArtistResponse(artist, albums); ++ } ++ ++ // Get local artist from Jellyfin ++ var jellyfinArtist = await _proxyService.GetArtistAsync(artistIdOrName, Request.Headers); ++ if (jellyfinArtist == null) ++ { ++ return _responseBuilder.CreateError(404, "Artist not found"); ++ } ++ ++ var artistData = _modelMapper.ParseArtist(jellyfinArtist.RootElement); ++ var artistName = artistData.Name; ++ var localArtistId = artistData.Id; ++ ++ // Get local albums ++ var localAlbumsResult = await _proxyService.GetItemsAsync( ++ parentId: null, ++ includeItemTypes: new[] { "MusicAlbum" }, ++ sortBy: "SortName", ++ clientHeaders: Request.Headers); ++ ++ var (_, localAlbums, _) = _modelMapper.ParseItemsResponse(localAlbumsResult); ++ ++ // Filter to just this artist's albums ++ var artistAlbums = localAlbums ++ .Where(a => a.ArtistId == localArtistId || ++ (a.Artist?.Equals(artistName, StringComparison.OrdinalIgnoreCase) ?? false)) ++ .ToList(); ++ ++ // Search for external albums by this artist ++ var externalArtists = await _metadataService.SearchArtistsAsync(artistName, 1); ++ var externalAlbums = new List(); ++ ++ if (externalArtists.Count > 0) ++ { ++ var extArtist = externalArtists[0]; ++ if (extArtist.Name.Equals(artistName, StringComparison.OrdinalIgnoreCase)) ++ { ++ externalAlbums = await _metadataService.GetArtistAlbumsAsync("deezer", extArtist.ExternalId!); ++ ++ // Set artist info to local artist so albums link back correctly ++ foreach (var a in externalAlbums) ++ { ++ if (string.IsNullOrEmpty(a.Artist)) a.Artist = artistName; ++ if (string.IsNullOrEmpty(a.ArtistId)) a.ArtistId = localArtistId; ++ } ++ } ++ } ++ ++ // Deduplicate albums by title ++ var localAlbumTitles = new HashSet(artistAlbums.Select(a => a.Title), StringComparer.OrdinalIgnoreCase); ++ var mergedAlbums = artistAlbums.ToList(); ++ mergedAlbums.AddRange(externalAlbums.Where(a => !localAlbumTitles.Contains(a.Title))); ++ ++ return _responseBuilder.CreateArtistResponse(artistData, mergedAlbums); ++ } ++ ++ #endregion ++ ++ #region Audio Streaming ++ ++ /// ++ /// Downloads/streams audio. Works with local and external content. ++ /// ++ [HttpGet("Items/{itemId}/Download")] ++ [HttpGet("Items/{itemId}/File")] ++ public async Task DownloadAudio(string itemId) ++ { ++ if (string.IsNullOrWhiteSpace(itemId)) ++ { ++ return BadRequest(new { error = "Missing item ID" }); ++ } ++ ++ var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); ++ ++ if (!isExternal) ++ { ++ // Build path for Jellyfin download/file endpoint ++ var endpoint = Request.Path.Value?.Contains("/File", StringComparison.OrdinalIgnoreCase) == true ? "File" : "Download"; ++ var fullPath = $"Items/{itemId}/{endpoint}"; ++ if (Request.QueryString.HasValue) ++ { ++ fullPath = $"{fullPath}{Request.QueryString.Value}"; ++ } ++ ++ return await ProxyJellyfinStream(fullPath, itemId); ++ } ++ ++ // Handle external content ++ return await StreamExternalContent(provider!, externalId!); ++ } ++ ++ /// ++ /// Streams audio for a given item. Downloads on-demand for external content. ++ /// ++ [HttpGet("Audio/{itemId}/stream")] ++ [HttpGet("Audio/{itemId}/stream.{container}")] ++ public async Task StreamAudio(string itemId, string? container = null) ++ { ++ if (string.IsNullOrWhiteSpace(itemId)) ++ { ++ return BadRequest(new { error = "Missing item ID" }); ++ } ++ ++ var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); ++ ++ if (!isExternal) ++ { ++ // Build path for Jellyfin stream ++ var fullPath = string.IsNullOrEmpty(container) ++ ? $"Audio/{itemId}/stream" ++ : $"Audio/{itemId}/stream.{container}"; ++ ++ if (Request.QueryString.HasValue) ++ { ++ fullPath = $"{fullPath}{Request.QueryString.Value}"; ++ } ++ ++ return await ProxyJellyfinStream(fullPath, itemId); ++ } ++ ++ // Handle external content ++ return await StreamExternalContent(provider!, externalId!); ++ } ++ ++ /// ++ /// Proxies a stream from Jellyfin with proper header forwarding. ++ /// ++ private async Task ProxyJellyfinStream(string path, string itemId) ++ { ++ var jellyfinUrl = $"{_settings.Url?.TrimEnd('/')}/{path}"; ++ ++ try ++ { ++ var request = new HttpRequestMessage(HttpMethod.Get, jellyfinUrl); ++ ++ // Forward auth headers ++ if (Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuth)) ++ { ++ request.Headers.TryAddWithoutValidation("X-Emby-Authorization", embyAuth.ToString()); ++ } ++ else if (Request.Headers.TryGetValue("Authorization", out var auth)) ++ { ++ request.Headers.TryAddWithoutValidation("Authorization", auth.ToString()); ++ } ++ ++ // Forward Range header for seeking ++ if (Request.Headers.TryGetValue("Range", out var range)) ++ { ++ request.Headers.TryAddWithoutValidation("Range", range.ToString()); ++ } ++ ++ var response = await _proxyService.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ _logger.LogError("Jellyfin stream failed: {StatusCode} for {ItemId}", response.StatusCode, itemId); ++ return StatusCode((int)response.StatusCode); ++ } ++ ++ // Set response status and headers ++ Response.StatusCode = (int)response.StatusCode; ++ ++ var contentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg"; ++ ++ if (response.Content.Headers.ContentRange != null) ++ { ++ Response.Headers["Content-Range"] = response.Content.Headers.ContentRange.ToString(); ++ } ++ ++ if (response.Headers.AcceptRanges != null) ++ { ++ Response.Headers["Accept-Ranges"] = string.Join(", ", response.Headers.AcceptRanges); ++ } ++ ++ if (response.Content.Headers.ContentLength.HasValue) ++ { ++ Response.Headers["Content-Length"] = response.Content.Headers.ContentLength.Value.ToString(); ++ } ++ ++ var stream = await response.Content.ReadAsStreamAsync(); ++ return File(stream, contentType); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to proxy stream from Jellyfin for {ItemId}", itemId); ++ return StatusCode(500, new { error = $"Streaming failed: {ex.Message}" }); ++ } ++ } ++ ++ /// ++ /// Streams external content, using cache if available or downloading on-demand. ++ /// ++ private async Task StreamExternalContent(string provider, string externalId) ++ { ++ // Check for locally cached file ++ var localPath = await _localLibraryService.GetLocalPathForExternalSongAsync(provider, externalId); ++ ++ if (localPath != null && System.IO.File.Exists(localPath)) ++ { ++ var stream = System.IO.File.OpenRead(localPath); ++ return File(stream, GetContentType(localPath), enableRangeProcessing: true); ++ } ++ ++ // Download and stream on-demand ++ try ++ { ++ var downloadStream = await _downloadService.DownloadAndStreamAsync( ++ provider, ++ externalId, ++ HttpContext.RequestAborted); ++ ++ return File(downloadStream, "audio/mpeg", enableRangeProcessing: true); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to stream external song {Provider}:{ExternalId}", provider, externalId); ++ return StatusCode(500, new { error = $"Streaming failed: {ex.Message}" }); ++ } ++ } ++ ++ /// ++ /// Universal audio endpoint that redirects to the stream endpoint. ++ /// ++ [HttpGet("Audio/{itemId}/universal")] ++ public Task UniversalAudio(string itemId) ++ { ++ return StreamAudio(itemId); ++ } ++ ++ #endregion ++ ++ #region Images ++ ++ /// ++ /// Gets the primary image for an item. ++ /// ++ [HttpGet("Items/{itemId}/Images/{imageType}")] ++ [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")] ++ public async Task GetImage( ++ string itemId, ++ string imageType, ++ int imageIndex = 0, ++ [FromQuery] int? maxWidth = null, ++ [FromQuery] int? maxHeight = null) ++ { ++ if (string.IsNullOrWhiteSpace(itemId)) ++ { ++ return NotFound(); ++ } ++ ++ // Check for external playlist ++ if (PlaylistIdHelper.IsExternalPlaylist(itemId)) ++ { ++ return await GetPlaylistImage(itemId); ++ } ++ ++ var (isExternal, provider, type, externalId) = _localLibraryService.ParseExternalId(itemId); ++ ++ if (!isExternal) ++ { ++ // Redirect to Jellyfin directly for local content images ++ var queryString = new List(); ++ if (maxWidth.HasValue) queryString.Add($"maxWidth={maxWidth.Value}"); ++ if (maxHeight.HasValue) queryString.Add($"maxHeight={maxHeight.Value}"); ++ ++ var path = $"Items/{itemId}/Images/{imageType}"; ++ if (imageIndex > 0) ++ { ++ path = $"Items/{itemId}/Images/{imageType}/{imageIndex}"; ++ } ++ ++ if (queryString.Any()) ++ { ++ path = $"{path}?{string.Join("&", queryString)}"; ++ } ++ ++ var jellyfinUrl = $"{_settings.Url?.TrimEnd('/')}/{path}"; ++ return Redirect(jellyfinUrl); ++ } ++ ++ // Get external cover art URL ++ string? coverUrl = type switch ++ { ++ "artist" => (await _metadataService.GetArtistAsync(provider!, externalId!))?.ImageUrl, ++ "album" => (await _metadataService.GetAlbumAsync(provider!, externalId!))?.CoverArtUrl, ++ "song" => (await _metadataService.GetSongAsync(provider!, externalId!))?.CoverArtUrl, ++ _ => null ++ }; ++ ++ if (string.IsNullOrEmpty(coverUrl)) ++ { ++ return NotFound(); ++ } ++ ++ // Fetch and return the image using the proxy service's HttpClient ++ try ++ { ++ var response = await _proxyService.HttpClient.GetAsync(coverUrl); ++ if (!response.IsSuccessStatusCode) ++ { ++ return NotFound(); ++ } ++ ++ var imageBytes = await response.Content.ReadAsByteArrayAsync(); ++ var contentType = response.Content.Headers.ContentType?.ToString() ?? "image/jpeg"; ++ return File(imageBytes, contentType); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to fetch cover art from {Url}", coverUrl); ++ return NotFound(); ++ } ++ } ++ ++ #endregion ++ ++ #region Favorites ++ ++ /// ++ /// Marks an item as favorite. For playlists, triggers a full download. ++ /// ++ [HttpPost("Users/{userId}/FavoriteItems/{itemId}")] ++ public async Task MarkFavorite(string userId, string itemId) ++ { ++ // Check if this is an external playlist - trigger download ++ if (PlaylistIdHelper.IsExternalPlaylist(itemId)) ++ { ++ if (_playlistSyncService == null) ++ { ++ return _responseBuilder.CreateError(500, "Playlist functionality not enabled"); ++ } ++ ++ _logger.LogInformation("Favoriting external playlist {PlaylistId}, triggering download", itemId); ++ ++ // Start download in background ++ _ = Task.Run(async () => ++ { ++ try ++ { ++ await _playlistSyncService.DownloadFullPlaylistAsync(itemId); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to download playlist {PlaylistId}", itemId); ++ } ++ }); ++ ++ return Ok(new { IsFavorite = true }); ++ } ++ ++ // Check if this is an external song/album ++ var (isExternal, _, _) = _localLibraryService.ParseSongId(itemId); ++ if (isExternal) ++ { ++ // External items don't exist in Jellyfin, so we can't favorite them there ++ // Just return success - the client will show it as favorited ++ _logger.LogDebug("Favoriting external item {ItemId} (not synced to Jellyfin)", itemId); ++ return Ok(new { IsFavorite = true }); ++ } ++ ++ // For local Jellyfin items, proxy the request through ++ var endpoint = $"Users/{userId}/FavoriteItems/{itemId}"; ++ ++ try ++ { ++ using var request = new HttpRequestMessage(HttpMethod.Post, $"{_settings.Url?.TrimEnd('/')}/{endpoint}"); ++ ++ // Forward client authentication ++ if (Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuth)) ++ { ++ request.Headers.TryAddWithoutValidation("X-Emby-Authorization", embyAuth.ToString()); ++ } ++ else if (Request.Headers.TryGetValue("Authorization", out var auth)) ++ { ++ request.Headers.TryAddWithoutValidation("Authorization", auth.ToString()); ++ } ++ ++ var response = await _proxyService.HttpClient.SendAsync(request); ++ ++ if (response.IsSuccessStatusCode) ++ { ++ return Ok(new { IsFavorite = true }); ++ } ++ ++ _logger.LogWarning("Failed to favorite item in Jellyfin: {StatusCode}", response.StatusCode); ++ return _responseBuilder.CreateError((int)response.StatusCode, "Failed to mark favorite"); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error favoriting item {ItemId}", itemId); ++ return _responseBuilder.CreateError(500, "Failed to mark favorite"); ++ } ++ } ++ ++ /// ++ /// Removes an item from favorites. ++ /// ++ [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] ++ public async Task UnmarkFavorite(string userId, string itemId) ++ { ++ // External items can't be unfavorited ++ var (isExternal, _, _) = _localLibraryService.ParseSongId(itemId); ++ if (isExternal || PlaylistIdHelper.IsExternalPlaylist(itemId)) ++ { ++ return Ok(new { IsFavorite = false }); ++ } ++ ++ // Proxy to Jellyfin to unfavorite ++ var url = $"Users/{userId}/FavoriteItems/{itemId}"; ++ ++ try ++ { ++ using var request = new HttpRequestMessage(HttpMethod.Delete, $"{_settings.Url?.TrimEnd('/')}/{url}"); ++ ++ // Forward client authentication ++ if (Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuth)) ++ { ++ request.Headers.TryAddWithoutValidation("X-Emby-Authorization", embyAuth.ToString()); ++ } ++ else if (Request.Headers.TryGetValue("Authorization", out var auth)) ++ { ++ request.Headers.TryAddWithoutValidation("Authorization", auth.ToString()); ++ } ++ ++ var response = await _proxyService.HttpClient.SendAsync(request); ++ ++ if (response.IsSuccessStatusCode) ++ { ++ return Ok(new { IsFavorite = false }); ++ } ++ ++ return _responseBuilder.CreateError(500, "Failed to unfavorite item"); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error unfavoriting item {ItemId}", itemId); ++ return _responseBuilder.CreateError(500, "Failed to unfavorite item"); ++ } ++ } ++ ++ #endregion ++ ++ #region Playlists ++ ++ /// ++ /// Gets playlist tracks displayed as an album. ++ /// ++ private async Task GetPlaylistAsAlbum(string playlistId) ++ { ++ try ++ { ++ var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); ++ ++ var playlist = await _metadataService.GetPlaylistAsync(provider, externalId); ++ if (playlist == null) ++ { ++ return _responseBuilder.CreateError(404, "Playlist not found"); ++ } ++ ++ var tracks = await _metadataService.GetPlaylistTracksAsync(provider, externalId); ++ ++ // Cache tracks for playlist sync ++ if (_playlistSyncService != null) ++ { ++ foreach (var track in tracks) ++ { ++ if (!string.IsNullOrEmpty(track.ExternalId)) ++ { ++ var trackId = $"ext-{provider}-{track.ExternalId}"; ++ _playlistSyncService.AddTrackToPlaylistCache(trackId, playlistId); ++ } ++ } ++ _logger.LogDebug("Cached {Count} tracks for playlist {PlaylistId}", tracks.Count, playlistId); ++ } ++ ++ return _responseBuilder.CreatePlaylistAsAlbumResponse(playlist, tracks); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error getting playlist {PlaylistId}", playlistId); ++ return _responseBuilder.CreateError(500, "Failed to get playlist"); ++ } ++ } ++ ++ /// ++ /// Gets playlist tracks as child items. ++ /// ++ private async Task GetPlaylistTracks(string playlistId) ++ { ++ try ++ { ++ var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); ++ var tracks = await _metadataService.GetPlaylistTracksAsync(provider, externalId); ++ ++ return _responseBuilder.CreateItemsResponse(tracks); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error getting playlist tracks {PlaylistId}", playlistId); ++ return _responseBuilder.CreateError(500, "Failed to get playlist tracks"); ++ } ++ } ++ ++ /// ++ /// Gets a playlist cover image. ++ /// ++ private async Task GetPlaylistImage(string playlistId) ++ { ++ try ++ { ++ var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); ++ var playlist = await _metadataService.GetPlaylistAsync(provider, externalId); ++ ++ if (playlist == null || string.IsNullOrEmpty(playlist.CoverUrl)) ++ { ++ return NotFound(); ++ } ++ ++ var response = await _proxyService.HttpClient.GetAsync(playlist.CoverUrl); ++ if (!response.IsSuccessStatusCode) ++ { ++ return NotFound(); ++ } ++ ++ var imageBytes = await response.Content.ReadAsByteArrayAsync(); ++ var contentType = response.Content.Headers.ContentType?.ToString() ?? "image/jpeg"; ++ return File(imageBytes, contentType); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to get playlist image {PlaylistId}", playlistId); ++ return NotFound(); ++ } ++ } ++ ++ #endregion ++ ++ #region Authentication ++ ++ /// ++ /// Authenticates a user by username and password. ++ /// This is the primary login endpoint for Jellyfin clients. ++ /// ++ [HttpPost("Users/AuthenticateByName")] ++ public async Task AuthenticateByName() ++ { ++ try ++ { ++ // Enable buffering to allow multiple reads of the request body ++ Request.EnableBuffering(); ++ ++ // Read the request body ++ using var reader = new StreamReader(Request.Body, leaveOpen: true); ++ var body = await reader.ReadToEndAsync(); ++ ++ // Reset stream position ++ Request.Body.Position = 0; ++ ++ _logger.LogInformation("Authentication request received"); ++ // DO NOT log request body or detailed headers - contains password ++ ++ // Forward to Jellyfin server with client headers ++ var result = await _proxyService.PostJsonAsync("Users/AuthenticateByName", body, Request.Headers); ++ ++ if (result == null) ++ { ++ _logger.LogWarning("Authentication failed - no response from Jellyfin"); ++ return Unauthorized(new { error = "Authentication failed" }); ++ } ++ ++ _logger.LogInformation("Authentication successful"); ++ return Content(result.RootElement.GetRawText(), "application/json"); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error during authentication"); ++ return StatusCode(500, new { error = $"Authentication error: {ex.Message}" }); ++ } ++ } ++ ++ #endregion ++ ++ #region Recommendations & Instant Mix ++ ++ /// ++ /// Gets similar items for a given item. ++ /// For external items, searches for similar content from the provider. ++ /// ++ [HttpGet("Items/{itemId}/Similar")] ++ [HttpGet("Songs/{itemId}/Similar")] ++ public async Task GetSimilarItems( ++ string itemId, ++ [FromQuery] int limit = 50, ++ [FromQuery] string? fields = null, ++ [FromQuery] string? userId = null) ++ { ++ var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); ++ ++ if (isExternal) ++ { ++ try ++ { ++ // Get the original song to find similar content ++ var song = await _metadataService.GetSongAsync(provider!, externalId!); ++ if (song == null) ++ { ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ Items = Array.Empty(), ++ TotalRecordCount = 0 ++ }); ++ } ++ ++ // Search for similar songs using artist and genre ++ var searchQuery = $"{song.Artist}"; ++ var searchResult = await _metadataService.SearchSongsAsync(searchQuery, limit); ++ ++ // Filter out the original song and convert to Jellyfin format ++ var similarSongs = searchResult ++ .Where(s => s.Id != itemId) ++ .Take(limit) ++ .Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)) ++ .ToList(); ++ ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ Items = similarSongs, ++ TotalRecordCount = similarSongs.Count ++ }); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to get similar items for external song {ItemId}", itemId); ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ Items = Array.Empty(), ++ TotalRecordCount = 0 ++ }); ++ } ++ } ++ ++ // For local items, proxy to Jellyfin ++ var queryParams = new Dictionary ++ { ++ ["limit"] = limit.ToString() ++ }; ++ ++ if (!string.IsNullOrEmpty(fields)) ++ { ++ queryParams["fields"] = fields; ++ } ++ ++ if (!string.IsNullOrEmpty(userId)) ++ { ++ queryParams["userId"] = userId; ++ } ++ ++ var result = await _proxyService.GetJsonAsync($"Items/{itemId}/Similar", queryParams, Request.Headers); ++ ++ if (result == null) ++ { ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ Items = Array.Empty(), ++ TotalRecordCount = 0 ++ }); ++ } ++ ++ return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); ++ } ++ ++ /// ++ /// Gets an instant mix for a given item. ++ /// For external items, creates a mix from the artist's other songs. ++ /// ++ [HttpGet("Songs/{itemId}/InstantMix")] ++ [HttpGet("Items/{itemId}/InstantMix")] ++ public async Task GetInstantMix( ++ string itemId, ++ [FromQuery] int limit = 50, ++ [FromQuery] string? fields = null, ++ [FromQuery] string? userId = null) ++ { ++ var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); ++ ++ if (isExternal) ++ { ++ try ++ { ++ // Get the original song ++ var song = await _metadataService.GetSongAsync(provider!, externalId!); ++ if (song == null) ++ { ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ Items = Array.Empty(), ++ TotalRecordCount = 0 ++ }); ++ } ++ ++ // Get artist's albums to build a mix ++ var mixSongs = new List(); ++ ++ // Try to get artist albums ++ if (!string.IsNullOrEmpty(song.ExternalProvider) && !string.IsNullOrEmpty(song.ArtistId)) ++ { ++ var artistExternalId = song.ArtistId.Replace($"ext-{song.ExternalProvider}-artist-", ""); ++ var albums = await _metadataService.GetArtistAlbumsAsync(song.ExternalProvider, artistExternalId); ++ ++ // Get songs from a few albums ++ foreach (var album in albums.Take(3)) ++ { ++ var fullAlbum = await _metadataService.GetAlbumAsync(song.ExternalProvider, album.ExternalId!); ++ if (fullAlbum != null) ++ { ++ mixSongs.AddRange(fullAlbum.Songs); ++ } ++ ++ if (mixSongs.Count >= limit) break; ++ } ++ } ++ ++ // If we don't have enough songs, search for more by the artist ++ if (mixSongs.Count < limit) ++ { ++ var searchResult = await _metadataService.SearchSongsAsync(song.Artist, limit); ++ mixSongs.AddRange(searchResult.Where(s => !mixSongs.Any(m => m.Id == s.Id))); ++ } ++ ++ // Shuffle and limit ++ var random = new Random(); ++ var shuffledMix = mixSongs ++ .Where(s => s.Id != itemId) // Exclude the seed song ++ .OrderBy(_ => random.Next()) ++ .Take(limit) ++ .Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)) ++ .ToList(); ++ ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ Items = shuffledMix, ++ TotalRecordCount = shuffledMix.Count ++ }); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to create instant mix for external song {ItemId}", itemId); ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ Items = Array.Empty(), ++ TotalRecordCount = 0 ++ }); ++ } ++ } ++ ++ // For local items, proxy to Jellyfin ++ var queryParams = new Dictionary ++ { ++ ["limit"] = limit.ToString() ++ }; ++ ++ if (!string.IsNullOrEmpty(fields)) ++ { ++ queryParams["fields"] = fields; ++ } ++ ++ if (!string.IsNullOrEmpty(userId)) ++ { ++ queryParams["userId"] = userId; ++ } ++ ++ var result = await _proxyService.GetJsonAsync($"Songs/{itemId}/InstantMix", queryParams, Request.Headers); ++ ++ if (result == null) ++ { ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ Items = Array.Empty(), ++ TotalRecordCount = 0 ++ }); ++ } ++ ++ return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); ++ } ++ ++ #endregion ++ ++ #region System & Proxy ++ ++ /// ++ /// Returns public server info. ++ /// ++ [HttpGet("System/Info/Public")] ++ public async Task GetPublicSystemInfo() ++ { ++ var (success, serverName, version) = await _proxyService.TestConnectionAsync(); ++ ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ LocalAddress = Request.Host.ToString(), ++ ServerName = serverName ?? "Allstarr", ++ Version = version ?? "1.0.0", ++ ProductName = "Allstarr (Jellyfin Proxy)", ++ OperatingSystem = Environment.OSVersion.Platform.ToString(), ++ Id = _settings.DeviceId, ++ StartupWizardCompleted = true ++ }); ++ } ++ ++ /// ++ /// Root path handler - redirects to Jellyfin web UI. ++ /// ++ [HttpGet("", Order = 99)] ++ public async Task ProxyRootRequest() ++ { ++ return await ProxyRequest("web/index.html"); ++ } ++ ++ /// ++ /// Catch-all endpoint that proxies unhandled requests to Jellyfin transparently. ++ /// This route has the lowest priority and should only match requests that don't have SearchTerm. ++ /// ++ [HttpGet("{**path}", Order = 100)] ++ [HttpPost("{**path}", Order = 100)] ++ public async Task ProxyRequest(string path) ++ { ++ // Handle non-JSON responses (robots.txt, etc.) ++ if (path.EndsWith(".txt", StringComparison.OrdinalIgnoreCase) || ++ path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) ++ { ++ var fullPath = path; ++ if (Request.QueryString.HasValue) ++ { ++ fullPath = $"{path}{Request.QueryString.Value}"; ++ } ++ ++ var url = $"{_settings.Url?.TrimEnd('/')}/{fullPath}"; ++ ++ try ++ { ++ var response = await _proxyService.HttpClient.GetAsync(url); ++ var content = await response.Content.ReadAsStringAsync(); ++ var contentType = response.Content.Headers.ContentType?.ToString() ?? "text/plain"; ++ return Content(content, contentType); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to proxy non-JSON request for {Path}", path); ++ return NotFound(); ++ } ++ } ++ ++ // Check if this is a search request that should be handled by specific endpoints ++ var searchTerm = Request.Query["SearchTerm"].ToString(); ++ ++ if (!string.IsNullOrWhiteSpace(searchTerm)) ++ { ++ _logger.LogInformation("ProxyRequest intercepting search request: Path={Path}, SearchTerm={SearchTerm}", path, searchTerm); ++ ++ // Item search: /users/{userId}/items or /items ++ if (path.EndsWith("/items", StringComparison.OrdinalIgnoreCase) || path.Equals("items", StringComparison.OrdinalIgnoreCase)) ++ { ++ _logger.LogInformation("Redirecting to SearchItems"); ++ return await SearchItems( ++ searchTerm: searchTerm, ++ includeItemTypes: Request.Query["IncludeItemTypes"], ++ limit: int.TryParse(Request.Query["Limit"], out var limit) ? limit : 100, ++ startIndex: int.TryParse(Request.Query["StartIndex"], out var start) ? start : 0, ++ parentId: Request.Query["ParentId"], ++ sortBy: Request.Query["SortBy"], ++ recursive: Request.Query["Recursive"].ToString().Equals("true", StringComparison.OrdinalIgnoreCase), ++ userId: path.Contains("/users/", StringComparison.OrdinalIgnoreCase) && path.Split('/').Length > 2 ? path.Split('/')[2] : null); ++ } ++ ++ // Artist search: /artists/albumartists or /artists ++ if (path.Contains("/artists", StringComparison.OrdinalIgnoreCase)) ++ { ++ _logger.LogInformation("Redirecting to GetArtists"); ++ return await GetArtists( ++ searchTerm: searchTerm, ++ limit: int.TryParse(Request.Query["Limit"], out var limit) ? limit : 50, ++ startIndex: int.TryParse(Request.Query["StartIndex"], out var start) ? start : 0); ++ } ++ } ++ ++ try ++ { ++ // Include query string in the path ++ var fullPath = path; ++ if (Request.QueryString.HasValue) ++ { ++ fullPath = $"{path}{Request.QueryString.Value}"; ++ } ++ ++ JsonDocument? result; ++ ++ if (HttpContext.Request.Method == HttpMethod.Post.Method) ++ { ++ // Enable buffering BEFORE any reads ++ Request.EnableBuffering(); ++ ++ // Log request details for debugging ++ _logger.LogInformation("POST request to {Path}: Method={Method}, ContentType={ContentType}, ContentLength={ContentLength}", ++ fullPath, Request.Method, Request.ContentType, Request.ContentLength); ++ ++ // Read body using StreamReader with proper encoding ++ string body; ++ using (var reader = new StreamReader(Request.Body, System.Text.Encoding.UTF8, leaveOpen: true)) ++ { ++ body = await reader.ReadToEndAsync(); ++ } ++ ++ // Reset stream position after reading ++ Request.Body.Position = 0; ++ ++ if (string.IsNullOrWhiteSpace(body)) ++ { ++ _logger.LogWarning("Empty POST body for {Path}, ContentLength={ContentLength}, ContentType={ContentType}", ++ fullPath, Request.ContentLength, Request.ContentType); ++ } ++ else ++ { ++ _logger.LogInformation("POST body for {Path}: {BodyLength} bytes, ContentType={ContentType}", ++ fullPath, body.Length, Request.ContentType); ++ ++ // Always log body content for playback endpoints to debug the issue ++ if (fullPath.Contains("Playing", StringComparison.OrdinalIgnoreCase)) ++ { ++ _logger.LogInformation("POST body content: {Body}", body); ++ } ++ } ++ ++ result = await _proxyService.PostJsonAsync(fullPath, body, Request.Headers); ++ } ++ else ++ { ++ // Forward GET requests transparently with authentication headers and query string ++ result = await _proxyService.GetJsonAsync(fullPath, null, Request.Headers); ++ } ++ ++ if (result == null) ++ { ++ // Return 204 No Content for successful requests with no body ++ // (e.g., /sessions/playing, /sessions/playing/progress) ++ return NoContent(); ++ } ++ ++ return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Proxy request failed for {Path}", path); ++ return _responseBuilder.CreateError(502, $"Proxy error: {ex.Message}"); ++ } ++ } ++ ++ #endregion ++ ++ #region Helpers ++ ++ private static string[]? ParseItemTypes(string? includeItemTypes) ++ { ++ if (string.IsNullOrWhiteSpace(includeItemTypes)) ++ { ++ return null; ++ } ++ ++ return includeItemTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); ++ } ++ ++ private static string GetContentType(string filePath) ++ { ++ var extension = Path.GetExtension(filePath).ToLowerInvariant(); ++ return extension switch ++ { ++ ".mp3" => "audio/mpeg", ++ ".flac" => "audio/flac", ++ ".ogg" => "audio/ogg", ++ ".m4a" => "audio/mp4", ++ ".wav" => "audio/wav", ++ ".aac" => "audio/aac", ++ _ => "audio/mpeg" ++ }; ++ } ++ ++ /// ++ /// Scores search results based on fuzzy matching against the query. ++ /// Returns items with their relevance scores. ++ /// External results get a small boost to prioritize the larger catalog. ++ /// ++ private static List<(T Item, int Score)> ScoreSearchResults( ++ string query, ++ List items, ++ Func primaryField, ++ Func secondaryField, ++ bool isExternal = false) ++ { ++ return items.Select(item => ++ { ++ var primary = primaryField(item) ?? ""; ++ var secondary = secondaryField(item) ?? ""; ++ ++ // Score against primary field (title/name) ++ var primaryScore = FuzzyMatcher.CalculateSimilarity(query, primary); ++ ++ // Score against secondary field (artist) if provided ++ var secondaryScore = string.IsNullOrEmpty(secondary) ++ ? 0 ++ : FuzzyMatcher.CalculateSimilarity(query, secondary); ++ ++ // Use the better of the two scores ++ var baseScore = Math.Max(primaryScore, secondaryScore); ++ ++ // Give external results a small boost (+5 points) to prioritize the larger catalog ++ // This means external results will rank slightly higher when scores are close ++ var finalScore = isExternal ? Math.Min(100, baseScore + 5) : baseScore; ++ ++ return (item, finalScore); ++ }).ToList(); ++ } ++ ++ #endregion ++} ++// force rebuild Sun Jan 25 13:22:47 EST 2026 +diff --git a/allstarr/Controllers/SubSonicController.cs b/allstarr/Controllers/SubSonicController.cs +new file mode 100644 +index 0000000..97183d4 +--- /dev/null ++++ b/allstarr/Controllers/SubSonicController.cs +@@ -0,0 +1,805 @@ ++using Microsoft.AspNetCore.Mvc; ++using System.Xml.Linq; ++using System.Text; ++using System.Text.Json; ++using Microsoft.Extensions.Options; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using allstarr.Services; ++using allstarr.Services.Common; ++using allstarr.Services.Local; ++using allstarr.Services.Subsonic; ++ ++namespace allstarr.Controllers; ++ ++[ApiController] ++[Route("")] ++public class SubsonicController : ControllerBase ++{ ++ private readonly SubsonicSettings _subsonicSettings; ++ private readonly IMusicMetadataService _metadataService; ++ private readonly ILocalLibraryService _localLibraryService; ++ private readonly IDownloadService _downloadService; ++ private readonly SubsonicRequestParser _requestParser; ++ private readonly SubsonicResponseBuilder _responseBuilder; ++ private readonly SubsonicModelMapper _modelMapper; ++ private readonly SubsonicProxyService _proxyService; ++ private readonly PlaylistSyncService? _playlistSyncService; ++ private readonly ILogger _logger; ++ ++ public SubsonicController( ++ IOptions subsonicSettings, ++ IMusicMetadataService metadataService, ++ ILocalLibraryService localLibraryService, ++ IDownloadService downloadService, ++ SubsonicRequestParser requestParser, ++ SubsonicResponseBuilder responseBuilder, ++ SubsonicModelMapper modelMapper, ++ SubsonicProxyService proxyService, ++ ILogger logger, ++ PlaylistSyncService? playlistSyncService = null) ++ { ++ _subsonicSettings = subsonicSettings.Value; ++ _metadataService = metadataService; ++ _localLibraryService = localLibraryService; ++ _downloadService = downloadService; ++ _requestParser = requestParser; ++ _responseBuilder = responseBuilder; ++ _modelMapper = modelMapper; ++ _proxyService = proxyService; ++ _playlistSyncService = playlistSyncService; ++ _logger = logger; ++ ++ if (string.IsNullOrWhiteSpace(_subsonicSettings.Url)) ++ { ++ throw new Exception("Error: Environment variable SUBSONIC_URL is not set."); ++ } ++ } ++ ++ // Extract all parameters (query + body) ++ private async Task> ExtractAllParameters() ++ { ++ return await _requestParser.ExtractAllParametersAsync(Request); ++ } ++ ++ /// ++ /// Merges local and external search results. ++ /// ++ [HttpGet, HttpPost] ++ [Route("rest/search3")] ++ [Route("rest/search3.view")] ++ public async Task Search3() ++ { ++ var parameters = await ExtractAllParameters(); ++ var query = parameters.GetValueOrDefault("query", ""); ++ var format = parameters.GetValueOrDefault("f", "xml"); ++ ++ var cleanQuery = query.Trim().Trim('"'); ++ ++ if (string.IsNullOrWhiteSpace(cleanQuery)) ++ { ++ try ++ { ++ var result = await _proxyService.RelayAsync("rest/search3", parameters); ++ var contentType = result.ContentType ?? $"application/{format}"; ++ return File(result.Body, contentType); ++ } ++ catch ++ { ++ return _responseBuilder.CreateResponse(format, "searchResult3", new { }); ++ } ++ } ++ ++ var subsonicTask = _proxyService.RelaySafeAsync("rest/search3", parameters); ++ var externalTask = _metadataService.SearchAllAsync( ++ cleanQuery, ++ int.TryParse(parameters.GetValueOrDefault("songCount", "20"), out var sc) ? sc : 20, ++ int.TryParse(parameters.GetValueOrDefault("albumCount", "20"), out var ac) ? ac : 20, ++ int.TryParse(parameters.GetValueOrDefault("artistCount", "20"), out var arc) ? arc : 20 ++ ); ++ ++ // Search playlists if enabled ++ Task> playlistTask = _subsonicSettings.EnableExternalPlaylists ++ ? _metadataService.SearchPlaylistsAsync(cleanQuery, ac) // Use same limit as albums ++ : Task.FromResult(new List()); ++ ++ await Task.WhenAll(subsonicTask, externalTask, playlistTask); ++ ++ var subsonicResult = await subsonicTask; ++ var externalResult = await externalTask; ++ var playlistResult = await playlistTask; ++ ++ return MergeSearchResults(subsonicResult, externalResult, playlistResult, format); ++ } ++ ++ /// ++ /// Downloads on-the-fly if needed. ++ /// ++ [HttpGet, HttpPost] ++ [Route("rest/stream")] ++ [Route("rest/stream.view")] ++ public async Task Stream() ++ { ++ var parameters = await ExtractAllParameters(); ++ var id = parameters.GetValueOrDefault("id", ""); ++ ++ if (string.IsNullOrWhiteSpace(id)) ++ { ++ return BadRequest(new { error = "Missing id parameter" }); ++ } ++ ++ var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(id); ++ ++ if (!isExternal) ++ { ++ return await _proxyService.RelayStreamAsync(parameters, HttpContext.RequestAborted); ++ } ++ ++ var localPath = await _localLibraryService.GetLocalPathForExternalSongAsync(provider!, externalId!); ++ ++ if (localPath != null && System.IO.File.Exists(localPath)) ++ { ++ var stream = System.IO.File.OpenRead(localPath); ++ return File(stream, GetContentType(localPath), enableRangeProcessing: true); ++ } ++ ++ try ++ { ++ var downloadStream = await _downloadService.DownloadAndStreamAsync(provider!, externalId!, HttpContext.RequestAborted); ++ return File(downloadStream, "audio/mpeg", enableRangeProcessing: true); ++ } ++ catch (Exception ex) ++ { ++ return StatusCode(500, new { error = $"Failed to stream: {ex.Message}" }); ++ } ++ } ++ ++ /// ++ /// Returns external song info if needed. ++ /// ++ [HttpGet, HttpPost] ++ [Route("rest/getSong")] ++ [Route("rest/getSong.view")] ++ public async Task GetSong() ++ { ++ var parameters = await ExtractAllParameters(); ++ var id = parameters.GetValueOrDefault("id", ""); ++ var format = parameters.GetValueOrDefault("f", "xml"); ++ ++ if (string.IsNullOrWhiteSpace(id)) ++ { ++ return _responseBuilder.CreateError(format, 10, "Missing id parameter"); ++ } ++ ++ var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(id); ++ ++ if (!isExternal) ++ { ++ var result = await _proxyService.RelayAsync("rest/getSong", parameters); ++ var contentType = result.ContentType ?? $"application/{format}"; ++ return File(result.Body, contentType); ++ } ++ ++ var song = await _metadataService.GetSongAsync(provider!, externalId!); ++ ++ if (song == null) ++ { ++ return _responseBuilder.CreateError(format, 70, "Song not found"); ++ } ++ ++ return _responseBuilder.CreateSongResponse(format, song); ++ } ++ ++ /// ++ /// Merges local and Deezer albums. ++ /// ++ [HttpGet, HttpPost] ++ [Route("rest/getArtist")] ++ [Route("rest/getArtist.view")] ++ public async Task GetArtist() ++ { ++ var parameters = await ExtractAllParameters(); ++ var id = parameters.GetValueOrDefault("id", ""); ++ var format = parameters.GetValueOrDefault("f", "xml"); ++ ++ if (string.IsNullOrWhiteSpace(id)) ++ { ++ return _responseBuilder.CreateError(format, 10, "Missing id parameter"); ++ } ++ ++ var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(id); ++ ++ if (isExternal) ++ { ++ var artist = await _metadataService.GetArtistAsync(provider!, externalId!); ++ if (artist == null) ++ { ++ return _responseBuilder.CreateError(format, 70, "Artist not found"); ++ } ++ ++ var albums = await _metadataService.GetArtistAlbumsAsync(provider!, externalId!); ++ ++ // Fill artist info for each album (Deezer API doesn't include it in artist/albums endpoint) ++ foreach (var album in albums) ++ { ++ if (string.IsNullOrEmpty(album.Artist)) ++ { ++ album.Artist = artist.Name; ++ } ++ if (string.IsNullOrEmpty(album.ArtistId)) ++ { ++ album.ArtistId = artist.Id; ++ } ++ } ++ ++ return _responseBuilder.CreateArtistResponse(format, artist, albums); ++ } ++ ++ var navidromeResult = await _proxyService.RelaySafeAsync("rest/getArtist", parameters); ++ ++ if (!navidromeResult.Success || navidromeResult.Body == null) ++ { ++ return _responseBuilder.CreateError(format, 70, "Artist not found"); ++ } ++ ++ var navidromeContent = Encoding.UTF8.GetString(navidromeResult.Body); ++ string artistName = ""; ++ string localArtistId = id; // Keep the local artist ID for merged albums ++ var localAlbums = new List(); ++ object? artistData = null; ++ ++ if (format == "json" || navidromeResult.ContentType?.Contains("json") == true) ++ { ++ var jsonDoc = JsonDocument.Parse(navidromeContent); ++ if (jsonDoc.RootElement.TryGetProperty("subsonic-response", out var response) && ++ response.TryGetProperty("artist", out var artistElement)) ++ { ++ artistName = artistElement.TryGetProperty("name", out var name) ? name.GetString() ?? "" : ""; ++ artistData = _responseBuilder.ConvertSubsonicJsonElement(artistElement, true); ++ ++ if (artistElement.TryGetProperty("album", out var albums)) ++ { ++ foreach (var album in albums.EnumerateArray()) ++ { ++ localAlbums.Add(_responseBuilder.ConvertSubsonicJsonElement(album, true)); ++ } ++ } ++ } ++ } ++ ++ if (string.IsNullOrEmpty(artistName) || artistData == null) ++ { ++ return File(navidromeResult.Body, navidromeResult.ContentType ?? "application/json"); ++ } ++ ++ var deezerArtists = await _metadataService.SearchArtistsAsync(artistName, 1); ++ var deezerAlbums = new List(); ++ ++ if (deezerArtists.Count > 0) ++ { ++ var deezerArtist = deezerArtists[0]; ++ if (deezerArtist.Name.Equals(artistName, StringComparison.OrdinalIgnoreCase)) ++ { ++ deezerAlbums = await _metadataService.GetArtistAlbumsAsync("deezer", deezerArtist.ExternalId!); ++ ++ // Fill artist info for each album (Deezer API doesn't include it in artist/albums endpoint) ++ // Use local artist ID and name so albums link back to the local artist ++ foreach (var album in deezerAlbums) ++ { ++ if (string.IsNullOrEmpty(album.Artist)) ++ { ++ album.Artist = artistName; ++ } ++ if (string.IsNullOrEmpty(album.ArtistId)) ++ { ++ album.ArtistId = localArtistId; ++ } ++ } ++ } ++ } ++ ++ var localAlbumNames = new HashSet(StringComparer.OrdinalIgnoreCase); ++ foreach (var album in localAlbums) ++ { ++ if (album is Dictionary dict && dict.TryGetValue("name", out var nameObj)) ++ { ++ localAlbumNames.Add(nameObj?.ToString() ?? ""); ++ } ++ } ++ ++ var mergedAlbums = localAlbums.ToList(); ++ foreach (var deezerAlbum in deezerAlbums) ++ { ++ if (!localAlbumNames.Contains(deezerAlbum.Title)) ++ { ++ mergedAlbums.Add(_responseBuilder.ConvertAlbumToJson(deezerAlbum)); ++ } ++ } ++ ++ if (artistData is Dictionary artistDict) ++ { ++ artistDict["album"] = mergedAlbums; ++ artistDict["albumCount"] = mergedAlbums.Count; ++ } ++ ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ status = "ok", ++ version = "1.16.1", ++ artist = artistData ++ }); ++ } ++ ++ /// ++ /// Enriches local albums with Deezer songs. ++ /// ++ [HttpGet, HttpPost] ++ [Route("rest/getAlbum")] ++ [Route("rest/getAlbum.view")] ++ public async Task GetAlbum() ++ { ++ var parameters = await ExtractAllParameters(); ++ var id = parameters.GetValueOrDefault("id", ""); ++ var format = parameters.GetValueOrDefault("f", "xml"); ++ ++ if (string.IsNullOrWhiteSpace(id)) ++ { ++ return _responseBuilder.CreateError(format, 10, "Missing id parameter"); ++ } ++ ++ // Check if this is an external playlist ++ if (PlaylistIdHelper.IsExternalPlaylist(id)) ++ { ++ try ++ { ++ var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(id); ++ ++ // Get playlist metadata ++ var playlist = await _metadataService.GetPlaylistAsync(provider, externalId); ++ if (playlist == null) ++ { ++ return _responseBuilder.CreateError(format, 70, "Playlist not found"); ++ } ++ ++ // Get playlist tracks ++ var tracks = await _metadataService.GetPlaylistTracksAsync(provider, externalId); ++ ++ // Add all tracks to playlist cache so when they're played, we know they belong to this playlist ++ if (_playlistSyncService != null) ++ { ++ foreach (var track in tracks) ++ { ++ if (!string.IsNullOrEmpty(track.ExternalId)) ++ { ++ var trackId = $"ext-{provider}-{track.ExternalId}"; ++ _playlistSyncService.AddTrackToPlaylistCache(trackId, id); ++ } ++ } ++ ++ _logger.LogDebug("Added {TrackCount} tracks to playlist cache for {PlaylistId}", tracks.Count, id); ++ } ++ ++ // Convert to album response (playlist as album) ++ return _responseBuilder.CreatePlaylistAsAlbumResponse(format, playlist, tracks); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error getting playlist {Id}", id); ++ return _responseBuilder.CreateError(format, 70, "Playlist not found"); ++ } ++ } ++ ++ var (isExternal, albumProvider, albumExternalId) = _localLibraryService.ParseSongId(id); ++ ++ if (isExternal) ++ { ++ var album = await _metadataService.GetAlbumAsync(albumProvider!, albumExternalId!); ++ ++ if (album == null) ++ { ++ return _responseBuilder.CreateError(format, 70, "Album not found"); ++ } ++ ++ return _responseBuilder.CreateAlbumResponse(format, album); ++ } ++ ++ var navidromeResult = await _proxyService.RelaySafeAsync("rest/getAlbum", parameters); ++ ++ if (!navidromeResult.Success || navidromeResult.Body == null) ++ { ++ return _responseBuilder.CreateError(format, 70, "Album not found"); ++ } ++ ++ var navidromeContent = Encoding.UTF8.GetString(navidromeResult.Body); ++ string albumName = ""; ++ string artistName = ""; ++ var localSongs = new List(); ++ object? albumData = null; ++ ++ if (format == "json" || navidromeResult.ContentType?.Contains("json") == true) ++ { ++ var jsonDoc = JsonDocument.Parse(navidromeContent); ++ if (jsonDoc.RootElement.TryGetProperty("subsonic-response", out var response) && ++ response.TryGetProperty("album", out var albumElement)) ++ { ++ albumName = albumElement.TryGetProperty("name", out var name) ? name.GetString() ?? "" : ""; ++ artistName = albumElement.TryGetProperty("artist", out var artist) ? artist.GetString() ?? "" : ""; ++ albumData = _responseBuilder.ConvertSubsonicJsonElement(albumElement, true); ++ ++ if (albumElement.TryGetProperty("song", out var songs)) ++ { ++ foreach (var song in songs.EnumerateArray()) ++ { ++ localSongs.Add(_responseBuilder.ConvertSubsonicJsonElement(song, true)); ++ } ++ } ++ } ++ } ++ ++ if (string.IsNullOrEmpty(albumName) || string.IsNullOrEmpty(artistName) || albumData == null) ++ { ++ return File(navidromeResult.Body, navidromeResult.ContentType ?? "application/json"); ++ } ++ ++ var searchQuery = $"{artistName} {albumName}"; ++ var deezerAlbums = await _metadataService.SearchAlbumsAsync(searchQuery, 5); ++ Album? deezerAlbum = null; ++ ++ // Find matching album on Deezer (exact match first) ++ foreach (var candidate in deezerAlbums) ++ { ++ if (candidate.Artist != null && ++ candidate.Artist.Equals(artistName, StringComparison.OrdinalIgnoreCase) && ++ candidate.Title.Equals(albumName, StringComparison.OrdinalIgnoreCase)) ++ { ++ deezerAlbum = await _metadataService.GetAlbumAsync("deezer", candidate.ExternalId!); ++ break; ++ } ++ } ++ ++ // Fallback to fuzzy match ++ if (deezerAlbum == null) ++ { ++ foreach (var candidate in deezerAlbums) ++ { ++ if (candidate.Artist != null && ++ candidate.Artist.Contains(artistName, StringComparison.OrdinalIgnoreCase) && ++ (candidate.Title.Contains(albumName, StringComparison.OrdinalIgnoreCase) || ++ albumName.Contains(candidate.Title, StringComparison.OrdinalIgnoreCase))) ++ { ++ deezerAlbum = await _metadataService.GetAlbumAsync("deezer", candidate.ExternalId!); ++ break; ++ } ++ } ++ } ++ ++ if (deezerAlbum != null && deezerAlbum.Songs.Count > 0) ++ { ++ var localSongTitles = new HashSet(StringComparer.OrdinalIgnoreCase); ++ foreach (var song in localSongs) ++ { ++ if (song is Dictionary dict && dict.TryGetValue("title", out var titleObj)) ++ { ++ localSongTitles.Add(titleObj?.ToString() ?? ""); ++ } ++ } ++ ++ var mergedSongs = localSongs.ToList(); ++ foreach (var deezerSong in deezerAlbum.Songs) ++ { ++ if (!localSongTitles.Contains(deezerSong.Title)) ++ { ++ mergedSongs.Add(_responseBuilder.ConvertSongToJson(deezerSong)); ++ } ++ } ++ ++ mergedSongs = mergedSongs ++ .OrderBy(s => s is Dictionary dict && dict.TryGetValue("track", out var track) ++ ? Convert.ToInt32(track) ++ : 0) ++ .ToList(); ++ ++ if (albumData is Dictionary albumDict) ++ { ++ albumDict["song"] = mergedSongs; ++ albumDict["songCount"] = mergedSongs.Count; ++ ++ var totalDuration = 0; ++ foreach (var song in mergedSongs) ++ { ++ if (song is Dictionary dict && dict.TryGetValue("duration", out var dur)) ++ { ++ totalDuration += Convert.ToInt32(dur); ++ } ++ } ++ albumDict["duration"] = totalDuration; ++ } ++ } ++ ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ status = "ok", ++ version = "1.16.1", ++ album = albumData ++ }); ++ } ++ ++ /// ++ /// Proxies external covers. Uses type from ID to determine which API to call. ++ /// Format: ext-{provider}-{type}-{id} (e.g., ext-deezer-artist-259, ext-deezer-album-96126) ++ /// ++ [HttpGet, HttpPost] ++ [Route("rest/getCoverArt")] ++ [Route("rest/getCoverArt.view")] ++ public async Task GetCoverArt() ++ { ++ var parameters = await ExtractAllParameters(); ++ var id = parameters.GetValueOrDefault("id", ""); ++ ++ if (string.IsNullOrWhiteSpace(id)) ++ { ++ return NotFound(); ++ } ++ ++ // Check if this is a playlist cover art request ++ if (PlaylistIdHelper.IsExternalPlaylist(id)) ++ { ++ try ++ { ++ var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(id); ++ var playlist = await _metadataService.GetPlaylistAsync(provider, externalId); ++ ++ if (playlist == null || string.IsNullOrEmpty(playlist.CoverUrl)) ++ { ++ return NotFound(); ++ } ++ ++ // Download and return the cover image ++ var imageResponse = await new HttpClient().GetAsync(playlist.CoverUrl); ++ if (!imageResponse.IsSuccessStatusCode) ++ { ++ return NotFound(); ++ } ++ ++ var imageBytes = await imageResponse.Content.ReadAsByteArrayAsync(); ++ var contentType = imageResponse.Content.Headers.ContentType?.ToString() ?? "image/jpeg"; ++ return File(imageBytes, contentType); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error getting playlist cover art for {Id}", id); ++ return NotFound(); ++ } ++ } ++ ++ var (isExternal, coverProvider, type, coverExternalId) = _localLibraryService.ParseExternalId(id); ++ ++ if (!isExternal) ++ { ++ try ++ { ++ var result = await _proxyService.RelayAsync("rest/getCoverArt", parameters); ++ var contentType = result.ContentType ?? "image/jpeg"; ++ return File(result.Body, contentType); ++ } ++ catch ++ { ++ return NotFound(); ++ } ++ } ++ ++ string? coverUrl = null; ++ ++ // Use type to determine which API to call first ++ switch (type) ++ { ++ case "artist": ++ var artist = await _metadataService.GetArtistAsync(coverProvider!, coverExternalId!); ++ if (artist?.ImageUrl != null) ++ { ++ coverUrl = artist.ImageUrl; ++ } ++ break; ++ ++ case "album": ++ var album = await _metadataService.GetAlbumAsync(coverProvider!, coverExternalId!); ++ if (album?.CoverArtUrl != null) ++ { ++ coverUrl = album.CoverArtUrl; ++ } ++ break; ++ ++ case "song": ++ default: ++ // For songs, try to get from song first, then album ++ var song = await _metadataService.GetSongAsync(coverProvider!, coverExternalId!); ++ if (song?.CoverArtUrl != null) ++ { ++ coverUrl = song.CoverArtUrl; ++ } ++ else ++ { ++ // Fallback: try album with same ID (legacy behavior) ++ var albumFallback = await _metadataService.GetAlbumAsync(coverProvider!, coverExternalId!); ++ if (albumFallback?.CoverArtUrl != null) ++ { ++ coverUrl = albumFallback.CoverArtUrl; ++ } ++ } ++ break; ++ } ++ ++ if (coverUrl != null) ++ { ++ using var httpClient = new HttpClient(); ++ var response = await httpClient.GetAsync(coverUrl); ++ if (response.IsSuccessStatusCode) ++ { ++ var imageBytes = await response.Content.ReadAsByteArrayAsync(); ++ var contentType = response.Content.Headers.ContentType?.ToString() ?? "image/jpeg"; ++ return File(imageBytes, contentType); ++ } ++ } ++ ++ return NotFound(); ++ } ++ ++ #region Helper Methods ++ ++ private IActionResult MergeSearchResults( ++ (byte[]? Body, string? ContentType, bool Success) subsonicResult, ++ SearchResult externalResult, ++ List playlistResult, ++ string format) ++ { ++ var (localSongs, localAlbums, localArtists) = subsonicResult.Success && subsonicResult.Body != null ++ ? _modelMapper.ParseSearchResponse(subsonicResult.Body, subsonicResult.ContentType) ++ : (new List(), new List(), new List()); ++ ++ var isJson = format == "json" || subsonicResult.ContentType?.Contains("json") == true; ++ var (mergedSongs, mergedAlbums, mergedArtists) = _modelMapper.MergeSearchResults( ++ localSongs, ++ localAlbums, ++ localArtists, ++ externalResult, ++ playlistResult, ++ isJson); ++ ++ if (isJson) ++ { ++ return _responseBuilder.CreateJsonResponse(new ++ { ++ status = "ok", ++ version = "1.16.1", ++ searchResult3 = new ++ { ++ song = mergedSongs, ++ album = mergedAlbums, ++ artist = mergedArtists ++ } ++ }); ++ } ++ else ++ { ++ var ns = XNamespace.Get("http://subsonic.org/restapi"); ++ var searchResult3 = new XElement(ns + "searchResult3"); ++ ++ foreach (var artist in mergedArtists.Cast()) ++ { ++ searchResult3.Add(artist); ++ } ++ foreach (var album in mergedAlbums.Cast()) ++ { ++ searchResult3.Add(album); ++ } ++ foreach (var song in mergedSongs.Cast()) ++ { ++ searchResult3.Add(song); ++ } ++ ++ var doc = new XDocument( ++ new XElement(ns + "subsonic-response", ++ new XAttribute("status", "ok"), ++ new XAttribute("version", "1.16.1"), ++ searchResult3 ++ ) ++ ); ++ ++ return Content(doc.ToString(), "application/xml"); ++ } ++ } ++ ++ private string GetContentType(string filePath) ++ { ++ var extension = Path.GetExtension(filePath).ToLowerInvariant(); ++ return extension switch ++ { ++ ".mp3" => "audio/mpeg", ++ ".flac" => "audio/flac", ++ ".ogg" => "audio/ogg", ++ ".m4a" => "audio/mp4", ++ ".wav" => "audio/wav", ++ ".aac" => "audio/aac", ++ _ => "audio/mpeg" ++ }; ++ } ++ ++ #endregion ++ ++ /// ++ /// Stars (favorites) an item. For playlists, this triggers a full download. ++ /// ++ [HttpGet, HttpPost] ++ [Route("rest/star")] ++ [Route("rest/star.view")] ++ public async Task Star() ++ { ++ var parameters = await ExtractAllParameters(); ++ var format = parameters.GetValueOrDefault("f", "xml"); ++ ++ // Check if this is a playlist ++ var playlistId = parameters.GetValueOrDefault("id", ""); ++ ++ if (!string.IsNullOrEmpty(playlistId) && PlaylistIdHelper.IsExternalPlaylist(playlistId)) ++ { ++ if (_playlistSyncService == null) ++ { ++ return _responseBuilder.CreateError(format, 0, "Playlist functionality is not enabled"); ++ } ++ ++ _logger.LogInformation("Starring external playlist {PlaylistId}, triggering download", playlistId); ++ ++ // Trigger playlist download in background ++ _ = Task.Run(async () => ++ { ++ try ++ { ++ await _playlistSyncService.DownloadFullPlaylistAsync(playlistId); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to download playlist {PlaylistId}", playlistId); ++ } ++ }); ++ ++ // Return success response immediately ++ return _responseBuilder.CreateResponse(format, "starred", new { }); ++ } ++ ++ // For non-playlist items, relay to real Subsonic server ++ try ++ { ++ var result = await _proxyService.RelayAsync("rest/star", parameters); ++ var contentType = result.ContentType ?? $"application/{format}"; ++ return File(result.Body, contentType); ++ } ++ catch (HttpRequestException ex) ++ { ++ return _responseBuilder.CreateError(format, 0, $"Error connecting to Subsonic server: {ex.Message}"); ++ } ++ } ++ ++ // Generic endpoint to handle all subsonic API calls ++ [HttpGet, HttpPost] ++ [Route("{**endpoint}")] ++ public async Task GenericEndpoint(string endpoint) ++ { ++ var parameters = await ExtractAllParameters(); ++ var format = parameters.GetValueOrDefault("f", "xml"); ++ ++ try ++ { ++ var result = await _proxyService.RelayAsync(endpoint, parameters); ++ var contentType = result.ContentType ?? $"application/{format}"; ++ return File(result.Body, contentType); ++ } ++ catch (HttpRequestException ex) ++ { ++ // Return Subsonic-compatible error response ++ return _responseBuilder.CreateError(format, 0, $"Error connecting to Subsonic server: {ex.Message}"); ++ } ++ } ++} +\ No newline at end of file +diff --git a/allstarr/Filters/JellyfinAuthFilter.cs b/allstarr/Filters/JellyfinAuthFilter.cs +new file mode 100644 +index 0000000..be8702b +--- /dev/null ++++ b/allstarr/Filters/JellyfinAuthFilter.cs +@@ -0,0 +1,240 @@ ++using Microsoft.AspNetCore.Mvc; ++using Microsoft.AspNetCore.Mvc.Filters; ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++using System.Text.Json; ++using System.Text.RegularExpressions; ++ ++namespace allstarr.Filters; ++ ++/// ++/// Authentication filter for Jellyfin API endpoints. ++/// Validates client credentials against configured username and API key. ++/// Clients can authenticate via: ++/// - Authorization header: MediaBrowser Token="apikey" ++/// - X-Emby-Token header ++/// - Query parameter: api_key ++/// - JSON body (for login endpoints): Username/Pw fields ++/// ++public partial class JellyfinAuthFilter : IAsyncActionFilter ++{ ++ private readonly JellyfinSettings _settings; ++ private readonly ILogger _logger; ++ ++ public JellyfinAuthFilter( ++ IOptions settings, ++ ILogger logger) ++ { ++ _settings = settings.Value; ++ _logger = logger; ++ } ++ ++ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) ++ { ++ // Skip auth if no credentials configured (open mode) ++ if (string.IsNullOrEmpty(_settings.ClientUsername) || string.IsNullOrEmpty(_settings.ApiKey)) ++ { ++ _logger.LogDebug("Auth skipped - no client credentials configured"); ++ await next(); ++ return; ++ } ++ ++ var request = context.HttpContext.Request; ++ ++ // Try to extract credentials from various sources ++ var (username, token) = await ExtractCredentialsAsync(request); ++ ++ // Validate credentials ++ if (!ValidateCredentials(username, token)) ++ { ++ _logger.LogWarning("Authentication failed for user '{Username}' from {IP}", ++ username ?? "unknown", ++ context.HttpContext.Connection.RemoteIpAddress); ++ ++ context.Result = new UnauthorizedObjectResult(new ++ { ++ error = "Invalid credentials", ++ message = "Authentication required. Provide valid username and API key." ++ }); ++ return; ++ } ++ ++ _logger.LogDebug("Authentication successful for user '{Username}'", username); ++ await next(); ++ } ++ ++ private async Task<(string? username, string? token)> ExtractCredentialsAsync(HttpRequest request) ++ { ++ string? username = null; ++ string? token = null; ++ ++ // 1. Check Authorization header (MediaBrowser format) ++ if (request.Headers.TryGetValue("Authorization", out var authHeader)) ++ { ++ var authValue = authHeader.ToString(); ++ ++ // Parse MediaBrowser auth header: MediaBrowser Client="...", Token="..." ++ if (authValue.StartsWith("MediaBrowser", StringComparison.OrdinalIgnoreCase)) ++ { ++ token = ExtractTokenFromMediaBrowser(authValue); ++ username = ExtractUserIdFromMediaBrowser(authValue); ++ } ++ // Basic auth: Basic base64(username:password) ++ else if (authValue.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) ++ { ++ (username, token) = ParseBasicAuth(authValue); ++ } ++ } ++ ++ // 2. Check X-Emby-Token header ++ if (string.IsNullOrEmpty(token) && request.Headers.TryGetValue("X-Emby-Token", out var embyToken)) ++ { ++ token = embyToken.ToString(); ++ } ++ ++ // 3. Check X-MediaBrowser-Token header ++ if (string.IsNullOrEmpty(token) && request.Headers.TryGetValue("X-MediaBrowser-Token", out var mbToken)) ++ { ++ token = mbToken.ToString(); ++ } ++ ++ // 4. Check X-Emby-Authorization header (alternative format) ++ if (string.IsNullOrEmpty(token) && request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuth)) ++ { ++ token = ExtractTokenFromMediaBrowser(embyAuth.ToString()); ++ if (string.IsNullOrEmpty(username)) ++ { ++ username = ExtractUserIdFromMediaBrowser(embyAuth.ToString()); ++ } ++ } ++ ++ // 5. Check query parameters ++ if (string.IsNullOrEmpty(token)) ++ { ++ token = request.Query["api_key"].FirstOrDefault() ++ ?? request.Query["ApiKey"].FirstOrDefault() ++ ?? request.Query["X-Emby-Token"].FirstOrDefault(); ++ } ++ ++ if (string.IsNullOrEmpty(username)) ++ { ++ username = request.Query["userId"].FirstOrDefault() ++ ?? request.Query["UserId"].FirstOrDefault() ++ ?? request.Query["u"].FirstOrDefault(); ++ } ++ ++ // 6. Check JSON body for login endpoints (Jellyfin: Username/Pw, Navidrome: username/password) ++ if ((string.IsNullOrEmpty(username) || string.IsNullOrEmpty(token)) && ++ request.ContentType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true && ++ request.ContentLength > 0) ++ { ++ var (bodyUsername, bodyPassword) = await ExtractCredentialsFromBodyAsync(request); ++ if (string.IsNullOrEmpty(username)) username = bodyUsername; ++ if (string.IsNullOrEmpty(token)) token = bodyPassword; ++ } ++ ++ return (username, token); ++ } ++ ++ private async Task<(string? username, string? password)> ExtractCredentialsFromBodyAsync(HttpRequest request) ++ { ++ try ++ { ++ request.EnableBuffering(); ++ request.Body.Position = 0; ++ ++ using var reader = new StreamReader(request.Body, leaveOpen: true); ++ var body = await reader.ReadToEndAsync(); ++ request.Body.Position = 0; ++ ++ if (string.IsNullOrEmpty(body)) return (null, null); ++ ++ using var doc = JsonDocument.Parse(body); ++ var root = doc.RootElement; ++ ++ // Try Jellyfin format: Username, Pw ++ string? username = null; ++ string? password = null; ++ ++ if (root.TryGetProperty("Username", out var usernameProp)) ++ username = usernameProp.GetString(); ++ else if (root.TryGetProperty("username", out var usernameLowerProp)) ++ username = usernameLowerProp.GetString(); ++ ++ if (root.TryGetProperty("Pw", out var pwProp)) ++ password = pwProp.GetString(); ++ else if (root.TryGetProperty("pw", out var pwLowerProp)) ++ password = pwLowerProp.GetString(); ++ else if (root.TryGetProperty("Password", out var passwordProp)) ++ password = passwordProp.GetString(); ++ else if (root.TryGetProperty("password", out var passwordLowerProp)) ++ password = passwordLowerProp.GetString(); ++ ++ return (username, password); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogDebug(ex, "Failed to parse credentials from request body"); ++ return (null, null); ++ } ++ } ++ ++ private string? ExtractTokenFromMediaBrowser(string header) ++ { ++ var match = TokenRegex().Match(header); ++ return match.Success ? match.Groups[1].Value : null; ++ } ++ ++ private string? ExtractUserIdFromMediaBrowser(string header) ++ { ++ var match = UserIdRegex().Match(header); ++ return match.Success ? match.Groups[1].Value : null; ++ } ++ ++ private static (string? username, string? password) ParseBasicAuth(string authHeader) ++ { ++ try ++ { ++ var base64 = authHeader["Basic ".Length..].Trim(); ++ var bytes = Convert.FromBase64String(base64); ++ var credentials = System.Text.Encoding.UTF8.GetString(bytes); ++ var parts = credentials.Split(':', 2); ++ ++ return parts.Length == 2 ? (parts[0], parts[1]) : (null, null); ++ } ++ catch ++ { ++ return (null, null); ++ } ++ } ++ ++ private bool ValidateCredentials(string? username, string? token) ++ { ++ // Must have token (API key used as password) ++ if (string.IsNullOrEmpty(token)) ++ { ++ return false; ++ } ++ ++ // Token must match API key ++ if (!string.Equals(token, _settings.ApiKey, StringComparison.Ordinal)) ++ { ++ return false; ++ } ++ ++ // If username provided, it must match configured client username ++ if (!string.IsNullOrEmpty(username) && ++ !string.Equals(username, _settings.ClientUsername, StringComparison.OrdinalIgnoreCase)) ++ { ++ return false; ++ } ++ ++ return true; ++ } ++ ++ [GeneratedRegex(@"Token=""([^""]+)""", RegexOptions.IgnoreCase)] ++ private static partial Regex TokenRegex(); ++ ++ [GeneratedRegex(@"UserId=""([^""]+)""", RegexOptions.IgnoreCase)] ++ private static partial Regex UserIdRegex(); ++} +diff --git a/allstarr/Middleware/GlobalExceptionHandler.cs b/allstarr/Middleware/GlobalExceptionHandler.cs +new file mode 100644 +index 0000000..84e1126 +--- /dev/null ++++ b/allstarr/Middleware/GlobalExceptionHandler.cs +@@ -0,0 +1,88 @@ ++using Microsoft.AspNetCore.Diagnostics; ++ ++namespace allstarr.Middleware; ++ ++/// ++/// Global exception handler that catches unhandled exceptions and returns appropriate Subsonic API error responses ++/// ++public class GlobalExceptionHandler : IExceptionHandler ++{ ++ private readonly ILogger _logger; ++ ++ public GlobalExceptionHandler(ILogger logger) ++ { ++ _logger = logger; ++ } ++ ++ public async ValueTask TryHandleAsync( ++ HttpContext httpContext, ++ Exception exception, ++ CancellationToken cancellationToken) ++ { ++ _logger.LogError(exception, "Unhandled exception occurred: {Message}", exception.Message); ++ ++ var (statusCode, subsonicErrorCode, errorMessage) = MapExceptionToResponse(exception); ++ ++ httpContext.Response.StatusCode = statusCode; ++ httpContext.Response.ContentType = "application/json"; ++ ++ var response = CreateSubsonicErrorResponse(subsonicErrorCode, errorMessage); ++ await httpContext.Response.WriteAsJsonAsync(response, cancellationToken); ++ ++ return true; ++ } ++ ++ /// ++ /// Maps exception types to HTTP status codes and Subsonic error codes ++ /// ++ private (int statusCode, int subsonicErrorCode, string message) MapExceptionToResponse(Exception exception) ++ { ++ return exception switch ++ { ++ // Not Found errors (404) ++ FileNotFoundException => (404, 70, "Resource not found"), ++ DirectoryNotFoundException => (404, 70, "Directory not found"), ++ ++ // Authentication errors (401) ++ UnauthorizedAccessException => (401, 40, "Wrong username or password"), ++ ++ // Bad Request errors (400) ++ ArgumentNullException => (400, 10, "Required parameter is missing"), ++ ArgumentException => (400, 10, "Invalid request"), ++ FormatException => (400, 10, "Invalid format"), ++ InvalidOperationException => (400, 10, "Operation not valid"), ++ ++ // External service errors (502) ++ HttpRequestException => (502, 0, "External service unavailable"), ++ TimeoutException => (504, 0, "Request timeout"), ++ ++ // Generic server error (500) ++ _ => (500, 0, "An internal server error occurred") ++ }; ++ } ++ ++ /// ++ /// Creates a Subsonic-compatible error response ++ /// Subsonic error codes: ++ /// 0 = Generic error ++ /// 10 = Required parameter missing ++ /// 20 = Incompatible Subsonic REST protocol version ++ /// 30 = Incompatible Subsonic REST protocol version (server) ++ /// 40 = Wrong username or password ++ /// 50 = User not authorized ++ /// 60 = Trial period for the Subsonic server is over ++ /// 70 = Requested data was not found ++ /// ++ private object CreateSubsonicErrorResponse(int code, string message) ++ { ++ return new Dictionary ++ { ++ ["subsonic-response"] = new ++ { ++ status = "failed", ++ version = "1.16.1", ++ error = new { code, message } ++ } ++ }; ++ } ++} +diff --git a/allstarr/Models/Domain/Album.cs b/allstarr/Models/Domain/Album.cs +new file mode 100644 +index 0000000..c5cc95c +--- /dev/null ++++ b/allstarr/Models/Domain/Album.cs +@@ -0,0 +1,20 @@ ++namespace allstarr.Models.Domain; ++ ++/// ++/// Represents an album ++/// ++public class Album ++{ ++ public string Id { get; set; } = string.Empty; ++ public string Title { get; set; } = string.Empty; ++ public string Artist { get; set; } = string.Empty; ++ public string? ArtistId { get; set; } ++ public int? Year { get; set; } ++ public int? SongCount { get; set; } ++ public string? CoverArtUrl { get; set; } ++ public string? Genre { get; set; } ++ public bool IsLocal { get; set; } ++ public string? ExternalProvider { get; set; } ++ public string? ExternalId { get; set; } ++ public List Songs { get; set; } = new(); ++} +diff --git a/allstarr/Models/Domain/Artist.cs b/allstarr/Models/Domain/Artist.cs +new file mode 100644 +index 0000000..5340a79 +--- /dev/null ++++ b/allstarr/Models/Domain/Artist.cs +@@ -0,0 +1,15 @@ ++namespace allstarr.Models.Domain; ++ ++/// ++/// Represents an artist ++/// ++public class Artist ++{ ++ public string Id { get; set; } = string.Empty; ++ public string Name { get; set; } = string.Empty; ++ public string? ImageUrl { get; set; } ++ public int? AlbumCount { get; set; } ++ public bool IsLocal { get; set; } ++ public string? ExternalProvider { get; set; } ++ public string? ExternalId { get; set; } ++} +diff --git a/allstarr/Models/Domain/Song.cs b/allstarr/Models/Domain/Song.cs +new file mode 100644 +index 0000000..87e5925 +--- /dev/null ++++ b/allstarr/Models/Domain/Song.cs +@@ -0,0 +1,97 @@ ++namespace allstarr.Models.Domain; ++ ++/// ++/// Represents a song (local or external) ++/// ++public class Song ++{ ++ /// ++ /// Unique ID. For external songs, prefixed with "ext-" + provider + "-" + external id ++ /// Example: "ext-deezer-123456" or "local-789" ++ /// ++ public string Id { get; set; } = string.Empty; ++ ++ public string Title { get; set; } = string.Empty; ++ public string Artist { get; set; } = string.Empty; ++ public string? ArtistId { get; set; } ++ public string Album { get; set; } = string.Empty; ++ public string? AlbumId { get; set; } ++ public int? Duration { get; set; } // In seconds ++ public int? Track { get; set; } ++ public int? DiscNumber { get; set; } ++ public int? TotalTracks { get; set; } ++ public int? Year { get; set; } ++ public string? Genre { get; set; } ++ public string? CoverArtUrl { get; set; } ++ ++ /// ++ /// High-resolution cover art URL (for embedding) ++ /// ++ public string? CoverArtUrlLarge { get; set; } ++ ++ /// ++ /// BPM (beats per minute) if available ++ /// ++ public int? Bpm { get; set; } ++ ++ /// ++ /// ISRC (International Standard Recording Code) ++ /// ++ public string? Isrc { get; set; } ++ ++ /// ++ /// Full release date (format: YYYY-MM-DD) ++ /// ++ public string? ReleaseDate { get; set; } ++ ++ /// ++ /// Album artist name (may differ from track artist) ++ /// ++ public string? AlbumArtist { get; set; } ++ ++ /// ++ /// Composer(s) ++ /// ++ public string? Composer { get; set; } ++ ++ /// ++ /// Album label ++ /// ++ public string? Label { get; set; } ++ ++ /// ++ /// Copyright ++ /// ++ public string? Copyright { get; set; } ++ ++ /// ++ /// Contributing artists (features, etc.) ++ /// ++ public List Contributors { get; set; } = new(); ++ ++ /// ++ /// Indicates whether the song is available locally or needs to be downloaded ++ /// ++ public bool IsLocal { get; set; } ++ ++ /// ++ /// External provider (deezer, spotify, etc.) - null if local ++ /// ++ public string? ExternalProvider { get; set; } ++ ++ /// ++ /// ID on the external provider (for downloading) ++ /// ++ public string? ExternalId { get; set; } ++ ++ /// ++ /// Local file path (if available) ++ /// ++ public string? LocalPath { get; set; } ++ ++ /// ++ /// Deezer explicit content lyrics value ++ /// 0 = Naturally clean, 1 = Explicit, 2 = Not applicable, 3 = Clean/edited version, 6/7 = Unknown ++ /// ++ public int? ExplicitContentLyrics { get; set; } ++} +diff --git a/allstarr/Models/Download/DownloadInfo.cs b/allstarr/Models/Download/DownloadInfo.cs +new file mode 100644 +index 0000000..b73d661 +--- /dev/null ++++ b/allstarr/Models/Download/DownloadInfo.cs +@@ -0,0 +1,17 @@ ++namespace allstarr.Models.Download; ++ ++/// ++/// Information about an ongoing or completed download ++/// ++public class DownloadInfo ++{ ++ public string SongId { get; set; } = string.Empty; ++ public string ExternalId { get; set; } = string.Empty; ++ public string ExternalProvider { get; set; } = string.Empty; ++ public DownloadStatus Status { get; set; } ++ public double Progress { get; set; } // 0.0 to 1.0 ++ public string? LocalPath { get; set; } ++ public string? ErrorMessage { get; set; } ++ public DateTime StartedAt { get; set; } ++ public DateTime? CompletedAt { get; set; } ++} +diff --git a/allstarr/Models/Download/DownloadStatus.cs b/allstarr/Models/Download/DownloadStatus.cs +new file mode 100644 +index 0000000..0e82e46 +--- /dev/null ++++ b/allstarr/Models/Download/DownloadStatus.cs +@@ -0,0 +1,12 @@ ++namespace allstarr.Models.Download; ++ ++/// ++/// Download status of a song ++/// ++public enum DownloadStatus ++{ ++ NotStarted, ++ InProgress, ++ Completed, ++ Failed ++} +diff --git a/allstarr/Models/Search/SearchResult.cs b/allstarr/Models/Search/SearchResult.cs +new file mode 100644 +index 0000000..633ddb0 +--- /dev/null ++++ b/allstarr/Models/Search/SearchResult.cs +@@ -0,0 +1,13 @@ ++namespace allstarr.Models.Search; ++ ++using allstarr.Models.Domain; ++ ++/// ++/// Search result combining local and external results ++/// ++public class SearchResult ++{ ++ public List Songs { get; set; } = new(); ++ public List Albums { get; set; } = new(); ++ public List Artists { get; set; } = new(); ++} +diff --git a/allstarr/Models/Settings/DeezerSettings.cs b/allstarr/Models/Settings/DeezerSettings.cs +new file mode 100644 +index 0000000..1fe35d3 +--- /dev/null ++++ b/allstarr/Models/Settings/DeezerSettings.cs +@@ -0,0 +1,25 @@ ++namespace allstarr.Models.Settings; ++ ++/// ++/// Configuration for the Deezer downloader and metadata service ++/// ++public class DeezerSettings ++{ ++ /// ++ /// Deezer ARL token (required for downloading) ++ /// Obtained from browser cookies after logging into deezer.com ++ /// ++ public string? Arl { get; set; } ++ ++ /// ++ /// Fallback ARL token (optional) ++ /// Used if the primary ARL token fails ++ /// ++ public string? ArlFallback { get; set; } ++ ++ /// ++ /// Preferred audio quality: FLAC, MP3_320, MP3_128 ++ /// If not specified or unavailable, the highest available quality will be used. ++ /// ++ public string? Quality { get; set; } ++} +diff --git a/allstarr/Models/Settings/JellyfinSettings.cs b/allstarr/Models/Settings/JellyfinSettings.cs +new file mode 100644 +index 0000000..22d212b +--- /dev/null ++++ b/allstarr/Models/Settings/JellyfinSettings.cs +@@ -0,0 +1,67 @@ ++namespace allstarr.Models.Settings; ++ ++/// ++/// Configuration for Jellyfin media server backend ++/// ++public class JellyfinSettings ++{ ++ /// ++ /// URL of the Jellyfin server ++ /// Environment variable: JELLYFIN_URL ++ /// ++ public string? Url { get; set; } ++ ++ /// ++ /// API key for authenticating with Jellyfin server ++ /// Environment variable: JELLYFIN_API_KEY ++ /// ++ public string? ApiKey { get; set; } ++ ++ /// ++ /// User ID for accessing Jellyfin library ++ /// Environment variable: JELLYFIN_USER_ID ++ /// ++ public string? UserId { get; set; } ++ ++ /// ++ /// Username that clients must provide to authenticate ++ /// Environment variable: JELLYFIN_CLIENT_USERNAME ++ /// ++ public string? ClientUsername { get; set; } ++ ++ /// ++ /// Music library ID in Jellyfin (optional, auto-detected if not specified) ++ /// Environment variable: JELLYFIN_LIBRARY_ID ++ /// ++ public string? LibraryId { get; set; } ++ ++ /// ++ /// Client name reported to Jellyfin ++ /// ++ public string ClientName { get; set; } = "Allstarr"; ++ ++ /// ++ /// Client version reported to Jellyfin ++ /// ++ public string ClientVersion { get; set; } = "1.0.0"; ++ ++ /// ++ /// Device ID reported to Jellyfin ++ /// ++ public string DeviceId { get; set; } = "allstarrrr-proxy"; ++ ++ /// ++ /// Device name reported to Jellyfin ++ /// ++ public string DeviceName { get; set; } = "Allstarr Proxy"; ++ ++ // Shared settings (same as SubsonicSettings) ++ ++ public ExplicitFilter ExplicitFilter { get; set; } = ExplicitFilter.All; ++ public DownloadMode DownloadMode { get; set; } = DownloadMode.Track; ++ public MusicService MusicService { get; set; } = MusicService.SquidWTF; ++ public StorageMode StorageMode { get; set; } = StorageMode.Permanent; ++ public int CacheDurationHours { get; set; } = 1; ++ public bool EnableExternalPlaylists { get; set; } = true; ++ public string PlaylistsDirectory { get; set; } = "playlists"; ++} +diff --git a/allstarr/Models/Settings/QobuzSettings.cs b/allstarr/Models/Settings/QobuzSettings.cs +new file mode 100644 +index 0000000..32a5f26 +--- /dev/null ++++ b/allstarr/Models/Settings/QobuzSettings.cs +@@ -0,0 +1,25 @@ ++namespace allstarr.Models.Settings; ++ ++/// ++/// Configuration for the Qobuz downloader and metadata service ++/// ++public class QobuzSettings ++{ ++ /// ++ /// Qobuz user authentication token ++ /// Obtained from browser's localStorage after logging into play.qobuz.com ++ /// ++ public string? UserAuthToken { get; set; } ++ ++ /// ++ /// Qobuz user ID ++ /// Obtained from browser's localStorage after logging into play.qobuz.com ++ /// ++ public string? UserId { get; set; } ++ ++ /// ++ /// Preferred audio quality: FLAC, MP3_320, MP3_128 ++ /// If not specified or unavailable, the highest available quality will be used. ++ /// ++ public string? Quality { get; set; } ++} +diff --git a/allstarr/Models/Settings/RedisSettings.cs b/allstarr/Models/Settings/RedisSettings.cs +new file mode 100644 +index 0000000..d118a95 +--- /dev/null ++++ b/allstarr/Models/Settings/RedisSettings.cs +@@ -0,0 +1,7 @@ ++namespace allstarr.Models.Settings; ++ ++public class RedisSettings ++{ ++ public bool Enabled { get; set; } = true; ++ public string ConnectionString { get; set; } = "localhost:6379"; ++} +diff --git a/allstarr/Models/Settings/SquidWTFSettings.cs b/allstarr/Models/Settings/SquidWTFSettings.cs +new file mode 100644 +index 0000000..a6875d1 +--- /dev/null ++++ b/allstarr/Models/Settings/SquidWTFSettings.cs +@@ -0,0 +1,17 @@ ++namespace allstarr.Models.Settings; ++ ++/// ++/// Configuration for the SquidWTF downloader and metadata service ++/// ++public class SquidWTFSettings ++{ ++ /// ++ /// No user auth should be needed for this site. ++ /// ++ ++ /// ++ /// Preferred audio quality: FLAC, MP3_320, MP3_128 ++ /// If not specified or unavailable, the highest available quality will be used. ++ /// ++ public string? Quality { get; set; } ++} +diff --git a/allstarr/Models/Settings/SubsonicSettings.cs b/allstarr/Models/Settings/SubsonicSettings.cs +new file mode 100644 +index 0000000..c1330bf +--- /dev/null ++++ b/allstarr/Models/Settings/SubsonicSettings.cs +@@ -0,0 +1,155 @@ ++namespace allstarr.Models.Settings; ++ ++/// ++/// Media server backend type ++/// ++public enum BackendType ++{ ++ /// ++ /// Subsonic-compatible server (Navidrome, Airsonic, etc.) ++ /// ++ Subsonic, ++ ++ /// ++ /// Jellyfin media server ++ /// ++ Jellyfin ++} ++ ++/// ++/// Download mode for tracks ++/// ++public enum DownloadMode ++{ ++ /// ++ /// Download only the requested track (default behavior) ++ /// ++ Track, ++ ++ /// ++ /// When a track is played, download the entire album in background ++ /// The requested track is downloaded first, then remaining tracks are queued ++ /// ++ Album ++} ++ ++/// ++/// Explicit content filter mode for Deezer tracks ++/// ++public enum ExplicitFilter ++{ ++ /// ++ /// Show all tracks (no filtering) ++ /// ++ All, ++ ++ /// ++ /// Exclude clean/edited versions (explicit_content_lyrics == 3) ++ /// Shows original explicit content and naturally clean content ++ /// ++ ExplicitOnly, ++ ++ /// ++ /// Only show clean content (explicit_content_lyrics == 0 or 3) ++ /// Excludes tracks with explicit_content_lyrics == 1 ++ /// ++ CleanOnly ++} ++ ++/// ++/// Storage mode for downloaded tracks ++/// ++public enum StorageMode ++{ ++ /// ++ /// Files are permanently stored in the library and registered in the database ++ /// ++ Permanent, ++ ++ /// ++ /// Files are stored in a temporary cache and automatically cleaned up ++ /// Not registered in the database, no Navidrome scan triggered ++ /// ++ Cache ++} ++ ++/// ++/// Music service provider ++/// ++public enum MusicService ++{ ++ /// ++ /// Deezer music service ++ /// ++ Deezer, ++ ++ /// ++ /// Qobuz music service ++ /// ++ Qobuz, ++ ++ /// ++ /// SquidWTF music service ++ /// ++ SquidWTF ++} ++ ++public class SubsonicSettings ++{ ++ public string? Url { get; set; } ++ ++ /// ++ /// Explicit content filter mode (default: All) ++ /// Environment variable: EXPLICIT_FILTER ++ /// Values: "All", "ExplicitOnly", "CleanOnly" ++ /// Note: Only works with Deezer ++ /// ++ public ExplicitFilter ExplicitFilter { get; set; } = ExplicitFilter.All; ++ ++ /// ++ /// Download mode for tracks (default: Track) ++ /// Environment variable: DOWNLOAD_MODE ++ /// Values: "Track" (download only played track), "Album" (download full album when playing a track) ++ /// ++ public DownloadMode DownloadMode { get; set; } = DownloadMode.Track; ++ ++ /// ++ /// Music service to use (default: Deezer) ++ /// Environment variable: MUSIC_SERVICE ++ /// Values: "Deezer", "Qobuz", "SquidWTF" ++ /// ++ ++ public MusicService MusicService { get; set; } = MusicService.SquidWTF; ++ ++ /// ++ /// Storage mode for downloaded files (default: Permanent) ++ /// Environment variable: STORAGE_MODE ++ /// Values: "Permanent" (files saved to library), "Cache" (temporary files, auto-cleanup) ++ /// ++ public StorageMode StorageMode { get; set; } = StorageMode.Permanent; ++ ++ /// ++ /// Cache duration in hours for Cache storage mode (default: 1) ++ /// Environment variable: CACHE_DURATION_HOURS ++ /// Files older than this duration will be automatically deleted ++ /// Only applies when StorageMode is Cache ++ /// ++ public int CacheDurationHours { get; set; } = 1; ++ ++ /// ++ /// Enable external playlist search and streaming (default: true) ++ /// Environment variable: ENABLE_EXTERNAL_PLAYLISTS ++ /// When enabled, users can search for playlists from the configured music provider ++ /// Playlists appear as "albums" in search results with genre "Playlist" ++ /// ++ public bool EnableExternalPlaylists { get; set; } = true; ++ ++ /// ++ /// Directory name for storing playlist .m3u files (default: "playlists") ++ /// Environment variable: PLAYLISTS_DIRECTORY ++ /// Relative to the music library root directory ++ /// Playlist files will be stored in {MusicDirectory}/{PlaylistsDirectory}/ ++ /// ++ public string PlaylistsDirectory { get; set; } = "playlists"; ++ ++} +diff --git a/allstarr/Models/Subsonic/ExternalPlaylist.cs b/allstarr/Models/Subsonic/ExternalPlaylist.cs +new file mode 100644 +index 0000000..d1db309 +--- /dev/null ++++ b/allstarr/Models/Subsonic/ExternalPlaylist.cs +@@ -0,0 +1,58 @@ ++namespace allstarr.Models.Subsonic; ++ ++/// ++/// Represents a playlist from an external music provider (Deezer, Qobuz). ++/// ++public class ExternalPlaylist ++{ ++ /// ++ /// Unique identifier in the format "pl-{provider}-{externalId}" ++ /// Example: "pl-deezer-123456" or "pl-qobuz-789" ++ /// ++ public string Id { get; set; } = string.Empty; ++ ++ /// ++ /// Playlist name ++ /// ++ public string Name { get; set; } = string.Empty; ++ ++ /// ++ /// Playlist description ++ /// ++ public string? Description { get; set; } ++ ++ /// ++ /// Name of the playlist creator/curator ++ /// ++ public string? CuratorName { get; set; } ++ ++ /// ++ /// Provider name ("deezer" or "qobuz") ++ /// ++ public string Provider { get; set; } = string.Empty; ++ ++ /// ++ /// External ID from the provider (without "pl-" prefix) ++ /// ++ public string ExternalId { get; set; } = string.Empty; ++ ++ /// ++ /// Number of tracks in the playlist ++ /// ++ public int TrackCount { get; set; } ++ ++ /// ++ /// Total duration in seconds ++ /// ++ public int Duration { get; set; } ++ ++ /// ++ /// Cover art URL from the provider ++ /// ++ public string? CoverUrl { get; set; } ++ ++ /// ++ /// Playlist creation date ++ /// ++ public DateTime? CreatedDate { get; set; } ++} +diff --git a/allstarr/Models/Subsonic/ScanStatus.cs b/allstarr/Models/Subsonic/ScanStatus.cs +new file mode 100644 +index 0000000..065a0ce +--- /dev/null ++++ b/allstarr/Models/Subsonic/ScanStatus.cs +@@ -0,0 +1,10 @@ ++namespace allstarr.Models.Subsonic; ++ ++/// ++/// Subsonic library scan status ++/// ++public class ScanStatus ++{ ++ public bool Scanning { get; set; } ++ public int? Count { get; set; } ++} +diff --git a/allstarr/Program.cs b/allstarr/Program.cs +new file mode 100644 +index 0000000..8013223 +--- /dev/null ++++ b/allstarr/Program.cs +@@ -0,0 +1,239 @@ ++using allstarr.Models.Settings; ++using allstarr.Services; ++using allstarr.Services.Deezer; ++using allstarr.Services.Qobuz; ++using allstarr.Services.SquidWTF; ++using allstarr.Services.Local; ++using allstarr.Services.Validation; ++using allstarr.Services.Subsonic; ++using allstarr.Services.Jellyfin; ++using allstarr.Services.Common; ++using allstarr.Middleware; ++using allstarr.Filters; ++ ++var builder = WebApplication.CreateBuilder(args); ++ ++// Determine backend type FIRST ++var backendType = builder.Configuration.GetValue("Backend:Type"); ++ ++// Configure Kestrel for large responses over VPN/Tailscale ++builder.WebHost.ConfigureKestrel(serverOptions => ++{ ++ serverOptions.Limits.MaxResponseBufferSize = null; // Disable response buffering limit ++ serverOptions.Limits.MaxRequestBodySize = null; // Allow large request bodies ++ serverOptions.Limits.MinResponseDataRate = null; // Disable minimum data rate for slow connections ++}); ++ ++// Add response compression for large JSON responses (helps with Tailscale/VPN MTU issues) ++builder.Services.AddResponseCompression(options => ++{ ++ options.EnableForHttps = true; ++ options.MimeTypes = new[] { "application/json", "text/json" }; ++}); ++ ++// Add services to the container - conditionally register controllers ++builder.Services.AddControllers() ++ .AddJsonOptions(options => ++ { ++ // Use original property names (PascalCase) to match Jellyfin API ++ options.JsonSerializerOptions.PropertyNamingPolicy = null; ++ options.JsonSerializerOptions.DictionaryKeyPolicy = null; ++ }) ++ .ConfigureApplicationPartManager(manager => ++ { ++ // Remove the default controller feature provider ++ var defaultProvider = manager.FeatureProviders ++ .OfType() ++ .FirstOrDefault(); ++ if (defaultProvider != null) ++ { ++ manager.FeatureProviders.Remove(defaultProvider); ++ } ++ // Add our custom provider that filters by backend type ++ manager.FeatureProviders.Add(new BackendControllerFeatureProvider(backendType)); ++ }); ++ ++builder.Services.AddHttpClient(); ++builder.Services.AddEndpointsApiExplorer(); ++builder.Services.AddSwaggerGen(); ++builder.Services.AddHttpContextAccessor(); ++ ++// Exception handling ++builder.Services.AddExceptionHandler(); ++builder.Services.AddProblemDetails(); ++ ++// Configuration - register both settings, active one determined by backend type ++builder.Services.Configure( ++ builder.Configuration.GetSection("Subsonic")); ++builder.Services.Configure( ++ builder.Configuration.GetSection("Jellyfin")); ++builder.Services.Configure( ++ builder.Configuration.GetSection("Deezer")); ++builder.Services.Configure( ++ builder.Configuration.GetSection("Qobuz")); ++builder.Services.Configure( ++ builder.Configuration.GetSection("Redis")); ++ ++// Get shared settings from the active backend config ++MusicService musicService; ++bool enableExternalPlaylists; ++ ++if (backendType == BackendType.Jellyfin) ++{ ++ musicService = builder.Configuration.GetValue("Jellyfin:MusicService"); ++ enableExternalPlaylists = builder.Configuration.GetValue("Jellyfin:EnableExternalPlaylists", true); ++} ++else ++{ ++ // Default to Subsonic ++ musicService = builder.Configuration.GetValue("Subsonic:MusicService"); ++ enableExternalPlaylists = builder.Configuration.GetValue("Subsonic:EnableExternalPlaylists", true); ++} ++ ++// Business services - shared across backends ++builder.Services.AddSingleton(); ++builder.Services.AddSingleton(); ++ ++// Register backend-specific services ++if (backendType == BackendType.Jellyfin) ++{ ++ // Jellyfin services ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++ builder.Services.AddScoped(); ++ builder.Services.AddScoped(); ++} ++else ++{ ++ // Subsonic services (default) ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++ builder.Services.AddScoped(); ++} ++ ++// Register music service based on configuration ++// IMPORTANT: Primary service MUST be registered LAST because ASP.NET Core DI ++// will use the last registered implementation when injecting IMusicMetadataService/IDownloadService ++if (musicService == MusicService.Qobuz) ++{ ++ // If playlists enabled, register Deezer FIRST (secondary provider) ++ if (enableExternalPlaylists) ++ { ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++ } ++ ++ // Qobuz services (primary) - registered LAST to be injected by default ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++} ++else if (musicService == MusicService.Deezer) ++{ ++ // If playlists enabled, register Qobuz FIRST (secondary provider) ++ if (enableExternalPlaylists) ++ { ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++ } ++ ++ // Deezer services (primary, default) - registered LAST to be injected by default ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++} ++else if (musicService == MusicService.SquidWTF) ++{ ++ // SquidWTF services ++ builder.Services.AddSingleton(); ++ builder.Services.AddSingleton(); ++} ++ ++// Startup validation - register validators based on backend ++if (backendType == BackendType.Jellyfin) ++{ ++ builder.Services.AddSingleton(); ++} ++else ++{ ++ builder.Services.AddSingleton(); ++} ++ ++builder.Services.AddSingleton(); ++builder.Services.AddSingleton(); ++builder.Services.AddSingleton(); ++ ++// Register orchestrator as hosted service ++builder.Services.AddHostedService(); ++ ++// Register cache cleanup service (only runs when StorageMode is Cache) ++builder.Services.AddHostedService(); ++ ++builder.Services.AddCors(options => ++{ ++ options.AddDefaultPolicy(policy => ++ { ++ policy.AllowAnyOrigin() ++ .AllowAnyMethod() ++ .AllowAnyHeader() ++ .WithExposedHeaders("X-Content-Duration", "X-Total-Count", "X-Nd-Authorization"); ++ }); ++}); ++ ++var app = builder.Build(); ++ ++// Configure the HTTP request pipeline. ++app.UseExceptionHandler(_ => { }); // Global exception handler ++ ++// Enable response compression EARLY in the pipeline ++app.UseResponseCompression(); ++ ++if (app.Environment.IsDevelopment()) ++{ ++ app.UseSwagger(); ++ app.UseSwaggerUI(); ++} ++ ++app.UseHttpsRedirection(); ++ ++app.UseAuthorization(); ++ ++app.UseCors(); ++ ++app.MapControllers(); ++ ++// Health check endpoint for monitoring ++app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow })); ++ ++app.Run(); ++ ++/// ++/// Controller feature provider that conditionally registers controllers based on backend type. ++/// This prevents route conflicts between JellyfinController and SubsonicController catch-all routes. ++/// ++class BackendControllerFeatureProvider : Microsoft.AspNetCore.Mvc.Controllers.ControllerFeatureProvider ++{ ++ private readonly BackendType _backendType; ++ ++ public BackendControllerFeatureProvider(BackendType backendType) ++ { ++ _backendType = backendType; ++ } ++ ++ protected override bool IsController(System.Reflection.TypeInfo typeInfo) ++ { ++ var isController = base.IsController(typeInfo); ++ if (!isController) return false; ++ ++ // Only register the controller matching the configured backend type ++ return _backendType switch ++ { ++ BackendType.Jellyfin => typeInfo.Name == "JellyfinController", ++ BackendType.Subsonic => typeInfo.Name == "SubsonicController", ++ _ => false ++ }; ++ } ++} +diff --git a/octo-fiesta/Properties/launchSettings.json b/allstarr/Properties/launchSettings.json +similarity index 100% +rename from octo-fiesta/Properties/launchSettings.json +rename to allstarr/Properties/launchSettings.json +diff --git a/allstarr/Services/Common/BaseDownloadService.cs b/allstarr/Services/Common/BaseDownloadService.cs +new file mode 100644 +index 0000000..4c2ae84 +--- /dev/null ++++ b/allstarr/Services/Common/BaseDownloadService.cs +@@ -0,0 +1,589 @@ ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using allstarr.Services.Local; ++using allstarr.Services.Subsonic; ++using TagLib; ++using IOFile = System.IO.File; ++ ++namespace allstarr.Services.Common; ++ ++/// ++/// Abstract base class for download services. ++/// Implements common download logic, tracking, and metadata writing. ++/// Subclasses implement provider-specific download and authentication logic. ++/// ++public abstract class BaseDownloadService : IDownloadService ++{ ++ protected readonly IConfiguration Configuration; ++ protected readonly ILocalLibraryService LocalLibraryService; ++ protected readonly IMusicMetadataService MetadataService; ++ protected readonly SubsonicSettings SubsonicSettings; ++ protected readonly ILogger Logger; ++ private readonly IServiceProvider _serviceProvider; ++ ++ protected readonly string DownloadPath; ++ protected readonly string CachePath; ++ ++ protected readonly Dictionary ActiveDownloads = new(); ++ protected readonly SemaphoreSlim DownloadLock = new(1, 1); ++ ++ /// ++ /// Lazy-loaded PlaylistSyncService to avoid circular dependency ++ /// ++ private PlaylistSyncService? _playlistSyncService; ++ protected PlaylistSyncService? PlaylistSyncService ++ { ++ get ++ { ++ if (_playlistSyncService == null) ++ { ++ _playlistSyncService = _serviceProvider.GetService(); ++ } ++ return _playlistSyncService; ++ } ++ } ++ ++ /// ++ /// Provider name (e.g., "deezer", "qobuz") ++ /// ++ protected abstract string ProviderName { get; } ++ ++ protected BaseDownloadService( ++ IConfiguration configuration, ++ ILocalLibraryService localLibraryService, ++ IMusicMetadataService metadataService, ++ SubsonicSettings subsonicSettings, ++ IServiceProvider serviceProvider, ++ ILogger logger) ++ { ++ Configuration = configuration; ++ LocalLibraryService = localLibraryService; ++ MetadataService = metadataService; ++ SubsonicSettings = subsonicSettings; ++ _serviceProvider = serviceProvider; ++ Logger = logger; ++ ++ DownloadPath = configuration["Library:DownloadPath"] ?? "./downloads"; ++ CachePath = PathHelper.GetCachePath(); ++ ++ if (!Directory.Exists(DownloadPath)) ++ { ++ Directory.CreateDirectory(DownloadPath); ++ } ++ ++ if (!Directory.Exists(CachePath)) ++ { ++ Directory.CreateDirectory(CachePath); ++ } ++ } ++ ++ #region IDownloadService Implementation ++ ++ public async Task DownloadSongAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default) ++ { ++ return await DownloadSongInternalAsync(externalProvider, externalId, triggerAlbumDownload: true, cancellationToken); ++ } ++ ++ public async Task DownloadAndStreamAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default) ++ { ++ // Check if already downloaded locally ++ var localPath = await LocalLibraryService.GetLocalPathForExternalSongAsync(externalProvider, externalId); ++ if (localPath != null && IOFile.Exists(localPath)) ++ { ++ Logger.LogInformation("Streaming from local cache: {Path}", localPath); ++ return IOFile.OpenRead(localPath); ++ } ++ ++ // For on-demand streaming, download to disk first to ensure complete file ++ // This is necessary because: ++ // 1. Clients may seek to arbitrary positions (requires full file) ++ // 2. Metadata embedding requires complete file ++ // 3. Caching for future plays ++ Logger.LogInformation("Downloading song for streaming: {Provider}:{ExternalId}", externalProvider, externalId); ++ localPath = await DownloadSongInternalAsync(externalProvider, externalId, triggerAlbumDownload: true, cancellationToken); ++ return IOFile.OpenRead(localPath); ++ } ++ ++ public DownloadInfo? GetDownloadStatus(string songId) ++ { ++ ActiveDownloads.TryGetValue(songId, out var info); ++ return info; ++ } ++ ++ public async Task GetLocalPathIfExistsAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != ProviderName) ++ { ++ return null; ++ } ++ ++ // Check local library ++ var localPath = await LocalLibraryService.GetLocalPathForExternalSongAsync(externalProvider, externalId); ++ if (localPath != null && IOFile.Exists(localPath)) ++ { ++ return localPath; ++ } ++ ++ // Check cache directory ++ var cachedPath = GetCachedFilePath(externalProvider, externalId); ++ if (cachedPath != null && IOFile.Exists(cachedPath)) ++ { ++ return cachedPath; ++ } ++ ++ return null; ++ } ++ ++ public abstract Task IsAvailableAsync(); ++ ++ public void DownloadRemainingAlbumTracksInBackground(string externalProvider, string albumExternalId, string excludeTrackExternalId) ++ { ++ if (externalProvider != ProviderName) ++ { ++ Logger.LogWarning("Provider '{Provider}' is not supported for album download", externalProvider); ++ return; ++ } ++ ++ _ = Task.Run(async () => ++ { ++ try ++ { ++ await DownloadRemainingAlbumTracksAsync(albumExternalId, excludeTrackExternalId); ++ } ++ catch (Exception ex) ++ { ++ Logger.LogError(ex, "Failed to download remaining album tracks for album {AlbumId}", albumExternalId); ++ } ++ }); ++ } ++ ++ #endregion ++ ++ #region Template Methods (to be implemented by subclasses) ++ ++ /// ++ /// Downloads a track and saves it to disk. ++ /// Subclasses implement provider-specific logic (encryption, authentication, etc.) ++ /// ++ /// External track ID ++ /// Song metadata ++ /// Cancellation token ++ /// Local file path where the track was saved ++ protected abstract Task DownloadTrackAsync(string trackId, Song song, CancellationToken cancellationToken); ++ ++ /// ++ /// Extracts the external album ID from the internal album ID format. ++ /// Example: "ext-deezer-album-123456" -> "123456" ++ /// ++ protected abstract string? ExtractExternalIdFromAlbumId(string albumId); ++ ++ #endregion ++ ++ #region Common Download Logic ++ ++ /// ++ /// Internal method for downloading a song with control over album download triggering ++ /// ++ protected async Task DownloadSongInternalAsync(string externalProvider, string externalId, bool triggerAlbumDownload, CancellationToken cancellationToken = default) ++ { ++ if (externalProvider != ProviderName) ++ { ++ throw new NotSupportedException($"Provider '{externalProvider}' is not supported"); ++ } ++ ++ var songId = $"ext-{externalProvider}-{externalId}"; ++ var isCache = SubsonicSettings.StorageMode == StorageMode.Cache; ++ ++ // Acquire lock BEFORE checking existence to prevent race conditions with concurrent requests ++ await DownloadLock.WaitAsync(cancellationToken); ++ ++ try ++ { ++ // Check if already downloaded (skip for cache mode as we want to check cache folder) ++ if (!isCache) ++ { ++ var existingPath = await LocalLibraryService.GetLocalPathForExternalSongAsync(externalProvider, externalId); ++ if (existingPath != null && IOFile.Exists(existingPath)) ++ { ++ Logger.LogInformation("Song already downloaded: {Path}", existingPath); ++ return existingPath; ++ } ++ } ++ else ++ { ++ // For cache mode, check if file exists in cache directory ++ var cachedPath = GetCachedFilePath(externalProvider, externalId); ++ if (cachedPath != null && IOFile.Exists(cachedPath)) ++ { ++ Logger.LogInformation("Song found in cache: {Path}", cachedPath); ++ // Update file access time for cache cleanup logic ++ IOFile.SetLastAccessTime(cachedPath, DateTime.UtcNow); ++ return cachedPath; ++ } ++ } ++ ++ // Check if download in progress ++ if (ActiveDownloads.TryGetValue(songId, out var activeDownload) && activeDownload.Status == DownloadStatus.InProgress) ++ { ++ Logger.LogInformation("Download already in progress for {SongId}, waiting...", songId); ++ // Release lock while waiting ++ DownloadLock.Release(); ++ ++ while (ActiveDownloads.TryGetValue(songId, out activeDownload) && activeDownload.Status == DownloadStatus.InProgress) ++ { ++ await Task.Delay(500, cancellationToken); ++ } ++ ++ if (activeDownload?.Status == DownloadStatus.Completed && activeDownload.LocalPath != null) ++ { ++ return activeDownload.LocalPath; ++ } ++ ++ throw new Exception(activeDownload?.ErrorMessage ?? "Download failed"); ++ } ++ ++ // Get metadata ++ // In Album mode, fetch the full album first to ensure AlbumArtist is correctly set ++ Song? song = null; ++ ++ if (SubsonicSettings.DownloadMode == DownloadMode.Album) ++ { ++ // First try to get the song to extract album ID ++ var tempSong = await MetadataService.GetSongAsync(externalProvider, externalId); ++ if (tempSong != null && !string.IsNullOrEmpty(tempSong.AlbumId)) ++ { ++ var albumExternalId = ExtractExternalIdFromAlbumId(tempSong.AlbumId); ++ if (!string.IsNullOrEmpty(albumExternalId)) ++ { ++ // Get full album with correct AlbumArtist ++ var album = await MetadataService.GetAlbumAsync(externalProvider, albumExternalId); ++ if (album != null) ++ { ++ // Find the track in the album ++ song = album.Songs.FirstOrDefault(s => s.ExternalId == externalId); ++ } ++ } ++ } ++ } ++ ++ // Fallback to individual song fetch if not in Album mode or album fetch failed ++ if (song == null) ++ { ++ song = await MetadataService.GetSongAsync(externalProvider, externalId); ++ } ++ ++ if (song == null) ++ { ++ throw new Exception("Song not found"); ++ } ++ ++ var downloadInfo = new DownloadInfo ++ { ++ SongId = songId, ++ ExternalId = externalId, ++ ExternalProvider = externalProvider, ++ Status = DownloadStatus.InProgress, ++ StartedAt = DateTime.UtcNow ++ }; ++ ActiveDownloads[songId] = downloadInfo; ++ ++ var localPath = await DownloadTrackAsync(externalId, song, cancellationToken); ++ ++ downloadInfo.Status = DownloadStatus.Completed; ++ downloadInfo.LocalPath = localPath; ++ downloadInfo.CompletedAt = DateTime.UtcNow; ++ ++ song.LocalPath = localPath; ++ ++ // Check if this track belongs to a playlist and update M3U ++ if (PlaylistSyncService != null) ++ { ++ try ++ { ++ var playlistId = PlaylistSyncService.GetPlaylistIdForTrack(songId); ++ if (playlistId != null) ++ { ++ Logger.LogInformation("Track {SongId} belongs to playlist {PlaylistId}, adding to M3U", songId, playlistId); ++ await PlaylistSyncService.AddTrackToM3UAsync(playlistId, song, localPath, isFullPlaylistDownload: false); ++ } ++ } ++ catch (Exception ex) ++ { ++ Logger.LogWarning(ex, "Failed to update playlist M3U for track {SongId}", songId); ++ } ++ } ++ ++ // Only register and scan if NOT in cache mode ++ if (!isCache) ++ { ++ await LocalLibraryService.RegisterDownloadedSongAsync(song, localPath); ++ ++ // Trigger a Subsonic library rescan (with debounce) ++ _ = Task.Run(async () => ++ { ++ try ++ { ++ await LocalLibraryService.TriggerLibraryScanAsync(); ++ } ++ catch (Exception ex) ++ { ++ Logger.LogWarning(ex, "Failed to trigger library scan after download"); ++ } ++ }); ++ ++ // If download mode is Album and triggering is enabled, start background download of remaining tracks ++ if (triggerAlbumDownload && SubsonicSettings.DownloadMode == DownloadMode.Album && !string.IsNullOrEmpty(song.AlbumId)) ++ { ++ var albumExternalId = ExtractExternalIdFromAlbumId(song.AlbumId); ++ if (!string.IsNullOrEmpty(albumExternalId)) ++ { ++ Logger.LogInformation("Download mode is Album, triggering background download for album {AlbumId}", albumExternalId); ++ DownloadRemainingAlbumTracksInBackground(externalProvider, albumExternalId, externalId); ++ } ++ } ++ } ++ else ++ { ++ Logger.LogInformation("Cache mode: skipping library registration and scan"); ++ } ++ ++ Logger.LogInformation("Download completed: {Path}", localPath); ++ return localPath; ++ } ++ catch (Exception ex) ++ { ++ if (ActiveDownloads.TryGetValue(songId, out var downloadInfo)) ++ { ++ downloadInfo.Status = DownloadStatus.Failed; ++ downloadInfo.ErrorMessage = ex.Message; ++ } ++ Logger.LogError(ex, "Download failed for {SongId}", songId); ++ throw; ++ } ++ finally ++ { ++ DownloadLock.Release(); ++ } ++ } ++ ++ protected async Task DownloadRemainingAlbumTracksAsync(string albumExternalId, string excludeTrackExternalId) ++ { ++ Logger.LogInformation("Starting background download for album {AlbumId} (excluding track {TrackId})", ++ albumExternalId, excludeTrackExternalId); ++ ++ var album = await MetadataService.GetAlbumAsync(ProviderName, albumExternalId); ++ if (album == null) ++ { ++ Logger.LogWarning("Album {AlbumId} not found, cannot download remaining tracks", albumExternalId); ++ return; ++ } ++ ++ var tracksToDownload = album.Songs ++ .Where(s => s.ExternalId != excludeTrackExternalId && !string.IsNullOrEmpty(s.ExternalId)) ++ .ToList(); ++ ++ Logger.LogInformation("Found {Count} additional tracks to download for album '{AlbumTitle}'", ++ tracksToDownload.Count, album.Title); ++ ++ foreach (var track in tracksToDownload) ++ { ++ try ++ { ++ var existingPath = await LocalLibraryService.GetLocalPathForExternalSongAsync(ProviderName, track.ExternalId!); ++ if (existingPath != null && IOFile.Exists(existingPath)) ++ { ++ Logger.LogDebug("Track {TrackId} already downloaded, skipping", track.ExternalId); ++ continue; ++ } ++ ++ // Check if download is already in progress or recently completed ++ var songId = $"ext-{ProviderName}-{track.ExternalId}"; ++ if (ActiveDownloads.TryGetValue(songId, out var activeDownload)) ++ { ++ if (activeDownload.Status == DownloadStatus.InProgress) ++ { ++ Logger.LogDebug("Track {TrackId} download already in progress, skipping", track.ExternalId); ++ continue; ++ } ++ ++ if (activeDownload.Status == DownloadStatus.Completed) ++ { ++ Logger.LogDebug("Track {TrackId} already downloaded in this session, skipping", track.ExternalId); ++ continue; ++ } ++ } ++ ++ Logger.LogInformation("Downloading track '{Title}' from album '{Album}'", track.Title, album.Title); ++ await DownloadSongInternalAsync(ProviderName, track.ExternalId!, triggerAlbumDownload: false, CancellationToken.None); ++ } ++ catch (Exception ex) ++ { ++ Logger.LogWarning(ex, "Failed to download track {TrackId} '{Title}'", track.ExternalId, track.Title); ++ } ++ } ++ ++ Logger.LogInformation("Completed background download for album '{AlbumTitle}'", album.Title); ++ } ++ ++ #endregion ++ ++ #region Common Metadata Writing ++ ++ /// ++ /// Writes ID3/Vorbis metadata and cover art to the audio file ++ /// ++ protected async Task WriteMetadataAsync(string filePath, Song song, CancellationToken cancellationToken) ++ { ++ try ++ { ++ Logger.LogInformation("Writing metadata to: {Path}", filePath); ++ ++ using var tagFile = TagLib.File.Create(filePath); ++ ++ // Basic metadata ++ tagFile.Tag.Title = song.Title; ++ tagFile.Tag.Performers = new[] { song.Artist }; ++ tagFile.Tag.Album = song.Album; ++ tagFile.Tag.AlbumArtists = new[] { !string.IsNullOrEmpty(song.AlbumArtist) ? song.AlbumArtist : song.Artist }; ++ ++ if (song.Track.HasValue) ++ tagFile.Tag.Track = (uint)song.Track.Value; ++ ++ if (song.TotalTracks.HasValue) ++ tagFile.Tag.TrackCount = (uint)song.TotalTracks.Value; ++ ++ if (song.DiscNumber.HasValue) ++ tagFile.Tag.Disc = (uint)song.DiscNumber.Value; ++ ++ if (song.Year.HasValue) ++ tagFile.Tag.Year = (uint)song.Year.Value; ++ ++ if (!string.IsNullOrEmpty(song.Genre)) ++ tagFile.Tag.Genres = new[] { song.Genre }; ++ ++ if (song.Bpm.HasValue) ++ tagFile.Tag.BeatsPerMinute = (uint)song.Bpm.Value; ++ ++ if (song.Contributors.Count > 0) ++ tagFile.Tag.Composers = song.Contributors.ToArray(); ++ ++ if (!string.IsNullOrEmpty(song.Copyright)) ++ tagFile.Tag.Copyright = song.Copyright; ++ ++ var comments = new List(); ++ if (!string.IsNullOrEmpty(song.Isrc)) ++ comments.Add($"ISRC: {song.Isrc}"); ++ ++ if (comments.Count > 0) ++ tagFile.Tag.Comment = string.Join(" | ", comments); ++ ++ // Download and embed cover art ++ var coverUrl = song.CoverArtUrlLarge ?? song.CoverArtUrl; ++ if (!string.IsNullOrEmpty(coverUrl)) ++ { ++ try ++ { ++ var coverData = await DownloadCoverArtAsync(coverUrl, cancellationToken); ++ if (coverData != null && coverData.Length > 0) ++ { ++ var mimeType = coverUrl.Contains(".png") ? "image/png" : "image/jpeg"; ++ var picture = new TagLib.Picture ++ { ++ Type = TagLib.PictureType.FrontCover, ++ MimeType = mimeType, ++ Description = "Cover", ++ Data = new TagLib.ByteVector(coverData) ++ }; ++ tagFile.Tag.Pictures = new TagLib.IPicture[] { picture }; ++ Logger.LogInformation("Cover art embedded: {Size} bytes", coverData.Length); ++ } ++ } ++ catch (Exception ex) ++ { ++ Logger.LogWarning(ex, "Failed to download cover art from {Url}", coverUrl); ++ } ++ } ++ ++ tagFile.Save(); ++ Logger.LogInformation("Metadata written successfully to: {Path}", filePath); ++ } ++ catch (Exception ex) ++ { ++ Logger.LogError(ex, "Failed to write metadata to: {Path}", filePath); ++ } ++ } ++ ++ /// ++ /// Downloads cover art from a URL ++ /// ++ protected async Task DownloadCoverArtAsync(string url, CancellationToken cancellationToken) ++ { ++ try ++ { ++ using var httpClient = new HttpClient(); ++ var response = await httpClient.GetAsync(url, cancellationToken); ++ response.EnsureSuccessStatusCode(); ++ return await response.Content.ReadAsByteArrayAsync(cancellationToken); ++ } ++ catch (Exception ex) ++ { ++ Logger.LogWarning(ex, "Failed to download cover art from {Url}", url); ++ return null; ++ } ++ } ++ ++ #endregion ++ ++ #region Utility Methods ++ ++ /// ++ /// Ensures a directory exists, creating it and all parent directories if necessary ++ /// ++ protected void EnsureDirectoryExists(string path) ++ { ++ try ++ { ++ if (!Directory.Exists(path)) ++ { ++ Directory.CreateDirectory(path); ++ Logger.LogDebug("Created directory: {Path}", path); ++ } ++ } ++ catch (Exception ex) ++ { ++ Logger.LogError(ex, "Failed to create directory: {Path}", path); ++ throw; ++ } ++ } ++ ++ /// ++ /// Gets the cached file path for a given provider and external ID ++ /// Returns null if no cached file exists ++ /// ++ protected string? GetCachedFilePath(string provider, string externalId) ++ { ++ try ++ { ++ // Search for cached files matching the pattern: {provider}_{externalId}.* ++ var pattern = $"{provider}_{externalId}.*"; ++ var files = Directory.GetFiles(CachePath, pattern, SearchOption.AllDirectories); ++ ++ if (files.Length > 0) ++ { ++ return files[0]; // Return first match ++ } ++ ++ return null; ++ } ++ catch (Exception ex) ++ { ++ Logger.LogWarning(ex, "Failed to search for cached file: {Provider}_{ExternalId}", provider, externalId); ++ return null; ++ } ++ } ++ ++ #endregion ++} +diff --git a/allstarr/Services/Common/CacheCleanupService.cs b/allstarr/Services/Common/CacheCleanupService.cs +new file mode 100644 +index 0000000..218f5b0 +--- /dev/null ++++ b/allstarr/Services/Common/CacheCleanupService.cs +@@ -0,0 +1,163 @@ ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++ ++namespace allstarr.Services.Common; ++ ++/// ++/// Background service that periodically cleans up old cached files ++/// Only runs when StorageMode is set to Cache ++/// ++public class CacheCleanupService : BackgroundService ++{ ++ private readonly IConfiguration _configuration; ++ private readonly SubsonicSettings _subsonicSettings; ++ private readonly ILogger _logger; ++ private readonly TimeSpan _cleanupInterval = TimeSpan.FromHours(1); ++ ++ public CacheCleanupService( ++ IConfiguration configuration, ++ IOptions subsonicSettings, ++ ILogger logger) ++ { ++ _configuration = configuration; ++ _subsonicSettings = subsonicSettings.Value; ++ _logger = logger; ++ } ++ ++ protected override async Task ExecuteAsync(CancellationToken stoppingToken) ++ { ++ // Only run if storage mode is Cache ++ if (_subsonicSettings.StorageMode != StorageMode.Cache) ++ { ++ _logger.LogInformation("CacheCleanupService disabled: StorageMode is not Cache"); ++ return; ++ } ++ ++ _logger.LogInformation("CacheCleanupService started with cleanup interval of {Interval} and retention of {Hours} hours", ++ _cleanupInterval, _subsonicSettings.CacheDurationHours); ++ ++ while (!stoppingToken.IsCancellationRequested) ++ { ++ try ++ { ++ await CleanupOldCachedFilesAsync(stoppingToken); ++ await Task.Delay(_cleanupInterval, stoppingToken); ++ } ++ catch (OperationCanceledException) ++ { ++ // Service is stopping, exit gracefully ++ break; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error during cache cleanup"); ++ // Continue running even if cleanup fails ++ await Task.Delay(_cleanupInterval, stoppingToken); ++ } ++ } ++ ++ _logger.LogInformation("CacheCleanupService stopped"); ++ } ++ ++ private async Task CleanupOldCachedFilesAsync(CancellationToken cancellationToken) ++ { ++ var cachePath = PathHelper.GetCachePath(); ++ ++ if (!Directory.Exists(cachePath)) ++ { ++ _logger.LogDebug("Cache directory does not exist: {Path}", cachePath); ++ return; ++ } ++ ++ var cutoffTime = DateTime.UtcNow.AddHours(-_subsonicSettings.CacheDurationHours); ++ var deletedCount = 0; ++ var totalSize = 0L; ++ ++ _logger.LogInformation("Starting cache cleanup: deleting files older than {CutoffTime}", cutoffTime); ++ ++ try ++ { ++ // Get all files in cache directory and subdirectories ++ var files = Directory.GetFiles(cachePath, "*.*", SearchOption.AllDirectories); ++ ++ foreach (var filePath in files) ++ { ++ if (cancellationToken.IsCancellationRequested) ++ break; ++ ++ try ++ { ++ var fileInfo = new FileInfo(filePath); ++ ++ // Use last access time to determine if file should be deleted ++ // This gets updated when a cached file is streamed ++ if (fileInfo.LastAccessTimeUtc < cutoffTime) ++ { ++ var size = fileInfo.Length; ++ File.Delete(filePath); ++ deletedCount++; ++ totalSize += size; ++ _logger.LogDebug("Deleted cached file: {Path} (last accessed: {LastAccess})", ++ filePath, fileInfo.LastAccessTimeUtc); ++ } ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to delete cached file: {Path}", filePath); ++ } ++ } ++ ++ // Clean up empty directories ++ await CleanupEmptyDirectoriesAsync(cachePath, cancellationToken); ++ ++ if (deletedCount > 0) ++ { ++ var sizeMB = totalSize / (1024.0 * 1024.0); ++ _logger.LogInformation("Cache cleanup completed: deleted {Count} files, freed {Size:F2} MB", ++ deletedCount, sizeMB); ++ } ++ else ++ { ++ _logger.LogDebug("Cache cleanup completed: no files to delete"); ++ } ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error during cache cleanup"); ++ } ++ } ++ ++ private async Task CleanupEmptyDirectoriesAsync(string rootPath, CancellationToken cancellationToken) ++ { ++ try ++ { ++ var directories = Directory.GetDirectories(rootPath, "*", SearchOption.AllDirectories) ++ .OrderByDescending(d => d.Length); // Process deepest directories first ++ ++ foreach (var directory in directories) ++ { ++ if (cancellationToken.IsCancellationRequested) ++ break; ++ ++ try ++ { ++ if (!Directory.EnumerateFileSystemEntries(directory).Any()) ++ { ++ Directory.Delete(directory); ++ _logger.LogDebug("Deleted empty directory: {Path}", directory); ++ } ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to delete empty directory: {Path}", directory); ++ } ++ } ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Error cleaning up empty directories"); ++ } ++ ++ await Task.CompletedTask; ++ } ++} +diff --git a/allstarr/Services/Common/Error.cs b/allstarr/Services/Common/Error.cs +new file mode 100644 +index 0000000..1aabfe1 +--- /dev/null ++++ b/allstarr/Services/Common/Error.cs +@@ -0,0 +1,140 @@ ++namespace allstarr.Services.Common; ++ ++/// ++/// Represents a typed error with code, message, and metadata ++/// ++public class Error ++{ ++ /// ++ /// Unique error code identifier ++ /// ++ public string Code { get; } ++ ++ /// ++ /// Human-readable error message ++ /// ++ public string Message { get; } ++ ++ /// ++ /// Error type/category ++ /// ++ public ErrorType Type { get; } ++ ++ /// ++ /// Additional metadata about the error ++ /// ++ public Dictionary? Metadata { get; } ++ ++ private Error(string code, string message, ErrorType type, Dictionary? metadata = null) ++ { ++ Code = code; ++ Message = message; ++ Type = type; ++ Metadata = metadata; ++ } ++ ++ /// ++ /// Creates a Not Found error (404) ++ /// ++ public static Error NotFound(string message, string? code = null, Dictionary? metadata = null) ++ { ++ return new Error(code ?? "NOT_FOUND", message, ErrorType.NotFound, metadata); ++ } ++ ++ /// ++ /// Creates a Validation error (400) ++ /// ++ public static Error Validation(string message, string? code = null, Dictionary? metadata = null) ++ { ++ return new Error(code ?? "VALIDATION_ERROR", message, ErrorType.Validation, metadata); ++ } ++ ++ /// ++ /// Creates an Unauthorized error (401) ++ /// ++ public static Error Unauthorized(string message, string? code = null, Dictionary? metadata = null) ++ { ++ return new Error(code ?? "UNAUTHORIZED", message, ErrorType.Unauthorized, metadata); ++ } ++ ++ /// ++ /// Creates a Forbidden error (403) ++ /// ++ public static Error Forbidden(string message, string? code = null, Dictionary? metadata = null) ++ { ++ return new Error(code ?? "FORBIDDEN", message, ErrorType.Forbidden, metadata); ++ } ++ ++ /// ++ /// Creates a Conflict error (409) ++ /// ++ public static Error Conflict(string message, string? code = null, Dictionary? metadata = null) ++ { ++ return new Error(code ?? "CONFLICT", message, ErrorType.Conflict, metadata); ++ } ++ ++ /// ++ /// Creates an Internal Server Error (500) ++ /// ++ public static Error Internal(string message, string? code = null, Dictionary? metadata = null) ++ { ++ return new Error(code ?? "INTERNAL_ERROR", message, ErrorType.Internal, metadata); ++ } ++ ++ /// ++ /// Creates an External Service Error (502/503) ++ /// ++ public static Error ExternalService(string message, string? code = null, Dictionary? metadata = null) ++ { ++ return new Error(code ?? "EXTERNAL_SERVICE_ERROR", message, ErrorType.ExternalService, metadata); ++ } ++ ++ /// ++ /// Creates a custom error with specified type ++ /// ++ public static Error Custom(string code, string message, ErrorType type, Dictionary? metadata = null) ++ { ++ return new Error(code, message, type, metadata); ++ } ++} ++ ++/// ++/// Categorizes error types for appropriate HTTP status code mapping ++/// ++public enum ErrorType ++{ ++ /// ++ /// Validation error (400 Bad Request) ++ /// ++ Validation, ++ ++ /// ++ /// Resource not found (404 Not Found) ++ /// ++ NotFound, ++ ++ /// ++ /// Authentication required (401 Unauthorized) ++ /// ++ Unauthorized, ++ ++ /// ++ /// Insufficient permissions (403 Forbidden) ++ /// ++ Forbidden, ++ ++ /// ++ /// Resource conflict (409 Conflict) ++ /// ++ Conflict, ++ ++ /// ++ /// Internal server error (500 Internal Server Error) ++ /// ++ Internal, ++ ++ /// ++ /// External service error (502 Bad Gateway / 503 Service Unavailable) ++ /// ++ ExternalService ++} +diff --git a/allstarr/Services/Common/FuzzyMatcher.cs b/allstarr/Services/Common/FuzzyMatcher.cs +new file mode 100644 +index 0000000..92664f0 +--- /dev/null ++++ b/allstarr/Services/Common/FuzzyMatcher.cs +@@ -0,0 +1,104 @@ ++namespace allstarr.Services.Common; ++ ++/// ++/// Provides fuzzy string matching for search result scoring. ++/// ++public static class FuzzyMatcher ++{ ++ /// ++ /// Calculates a similarity score between two strings (0-100). ++ /// Higher score means better match. ++ /// ++ public static int CalculateSimilarity(string query, string target) ++ { ++ if (string.IsNullOrWhiteSpace(query) || string.IsNullOrWhiteSpace(target)) ++ { ++ return 0; ++ } ++ ++ var queryLower = query.ToLowerInvariant().Trim(); ++ var targetLower = target.ToLowerInvariant().Trim(); ++ ++ // Exact match ++ if (queryLower == targetLower) ++ { ++ return 100; ++ } ++ ++ // Starts with query ++ if (targetLower.StartsWith(queryLower)) ++ { ++ return 90; ++ } ++ ++ // Contains query as whole word ++ if (targetLower.Contains($" {queryLower} ") || ++ targetLower.StartsWith($"{queryLower} ") || ++ targetLower.EndsWith($" {queryLower}")) ++ { ++ return 80; ++ } ++ ++ // Contains query anywhere ++ if (targetLower.Contains(queryLower)) ++ { ++ return 70; ++ } ++ ++ // Calculate Levenshtein distance for fuzzy matching ++ var distance = LevenshteinDistance(queryLower, targetLower); ++ var maxLength = Math.Max(queryLower.Length, targetLower.Length); ++ ++ if (maxLength == 0) ++ { ++ return 100; ++ } ++ ++ // Convert distance to similarity score (0-60 range for fuzzy matches) ++ var similarity = (1.0 - (double)distance / maxLength) * 60; ++ return (int)Math.Max(0, similarity); ++ } ++ ++ /// ++ /// Calculates Levenshtein distance between two strings. ++ /// ++ private static int LevenshteinDistance(string source, string target) ++ { ++ if (string.IsNullOrEmpty(source)) ++ { ++ return target?.Length ?? 0; ++ } ++ ++ if (string.IsNullOrEmpty(target)) ++ { ++ return source.Length; ++ } ++ ++ var sourceLength = source.Length; ++ var targetLength = target.Length; ++ var distance = new int[sourceLength + 1, targetLength + 1]; ++ ++ for (var i = 0; i <= sourceLength; i++) ++ { ++ distance[i, 0] = i; ++ } ++ ++ for (var j = 0; j <= targetLength; j++) ++ { ++ distance[0, j] = j; ++ } ++ ++ for (var i = 1; i <= sourceLength; i++) ++ { ++ for (var j = 1; j <= targetLength; j++) ++ { ++ var cost = target[j - 1] == source[i - 1] ? 0 : 1; ++ distance[i, j] = Math.Min( ++ Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1), ++ distance[i - 1, j - 1] + cost); ++ } ++ } ++ ++ return distance[sourceLength, targetLength]; ++ } ++} +diff --git a/allstarr/Services/Common/PathHelper.cs b/allstarr/Services/Common/PathHelper.cs +new file mode 100644 +index 0000000..35c48f0 +--- /dev/null ++++ b/allstarr/Services/Common/PathHelper.cs +@@ -0,0 +1,136 @@ ++using IOFile = System.IO.File; ++ ++namespace allstarr.Services.Common; ++ ++/// ++/// Helper class for path building and sanitization. ++/// Provides utilities for creating safe file and folder paths for downloaded music files. ++/// ++public static class PathHelper ++{ ++ /// ++ /// Gets the cache directory path for temporary file storage. ++ /// Uses system temp directory combined with allstarr-cache subfolder. ++ /// Respects TMPDIR environment variable on Linux/macOS. ++ /// ++ /// Full path to the cache directory. ++ public static string GetCachePath() ++ { ++ return Path.Combine(Path.GetTempPath(), "allstarr-cache"); ++ } ++ ++ /// ++ /// Builds the output path for a downloaded track following the Artist/Album/Track structure. ++ /// ++ /// Base download directory path. ++ /// Artist name (will be sanitized). ++ /// Album name (will be sanitized). ++ /// Track title (will be sanitized). ++ /// Optional track number for prefix. ++ /// File extension (e.g., ".flac", ".mp3"). ++ /// Full path for the track file. ++ public static string BuildTrackPath(string downloadPath, string artist, string album, string title, int? trackNumber, string extension) ++ { ++ var safeArtist = SanitizeFolderName(artist); ++ var safeAlbum = SanitizeFolderName(album); ++ var safeTitle = SanitizeFileName(title); ++ ++ var artistFolder = Path.Combine(downloadPath, safeArtist); ++ var albumFolder = Path.Combine(artistFolder, safeAlbum); ++ ++ var trackPrefix = trackNumber.HasValue ? $"{trackNumber:D2} - " : ""; ++ var fileName = $"{trackPrefix}{safeTitle}{extension}"; ++ ++ return Path.Combine(albumFolder, fileName); ++ } ++ ++ /// ++ /// Sanitizes a file name by removing invalid characters. ++ /// ++ /// Original file name. ++ /// Sanitized file name safe for all file systems. ++ public static string SanitizeFileName(string fileName) ++ { ++ if (string.IsNullOrWhiteSpace(fileName)) ++ { ++ return "Unknown"; ++ } ++ ++ var invalidChars = Path.GetInvalidFileNameChars(); ++ var sanitized = new string(fileName ++ .Select(c => invalidChars.Contains(c) ? '_' : c) ++ .ToArray()); ++ ++ if (sanitized.Length > 100) ++ { ++ sanitized = sanitized[..100]; ++ } ++ ++ return sanitized.Trim(); ++ } ++ ++ /// ++ /// Sanitizes a folder name by removing invalid path characters. ++ /// ++ /// Original folder name. ++ /// Sanitized folder name safe for all file systems. ++ public static string SanitizeFolderName(string folderName) ++ { ++ if (string.IsNullOrWhiteSpace(folderName)) ++ { ++ return "Unknown"; ++ } ++ ++ var invalidChars = Path.GetInvalidFileNameChars() ++ .Concat(Path.GetInvalidPathChars()) ++ .Distinct() ++ .ToArray(); ++ ++ var sanitized = new string(folderName ++ .Select(c => invalidChars.Contains(c) ? '_' : c) ++ .ToArray()); ++ ++ // Remove leading/trailing dots and spaces (Windows folder restrictions) ++ sanitized = sanitized.Trim().TrimEnd('.'); ++ ++ if (sanitized.Length > 100) ++ { ++ sanitized = sanitized[..100].TrimEnd('.'); ++ } ++ ++ // Ensure we have a valid name ++ if (string.IsNullOrWhiteSpace(sanitized)) ++ { ++ return "Unknown"; ++ } ++ ++ return sanitized; ++ } ++ ++ /// ++ /// Resolves a unique file path by appending a counter if the file already exists. ++ /// ++ /// Desired file path. ++ /// Unique file path that does not exist yet. ++ public static string ResolveUniquePath(string basePath) ++ { ++ if (!IOFile.Exists(basePath)) ++ { ++ return basePath; ++ } ++ ++ var directory = Path.GetDirectoryName(basePath)!; ++ var extension = Path.GetExtension(basePath); ++ var fileNameWithoutExt = Path.GetFileNameWithoutExtension(basePath); ++ ++ var counter = 1; ++ string uniquePath; ++ do ++ { ++ uniquePath = Path.Combine(directory, $"{fileNameWithoutExt} ({counter}){extension}"); ++ counter++; ++ } while (IOFile.Exists(uniquePath)); ++ ++ return uniquePath; ++ } ++} +diff --git a/allstarr/Services/Common/PlaylistIdHelper.cs b/allstarr/Services/Common/PlaylistIdHelper.cs +new file mode 100644 +index 0000000..2e155bb +--- /dev/null ++++ b/allstarr/Services/Common/PlaylistIdHelper.cs +@@ -0,0 +1,76 @@ ++namespace allstarr.Services.Common; ++ ++/// ++/// Helper class for handling external playlist IDs. ++/// Playlist IDs use the format: "pl-{provider}-{externalId}" ++/// Example: "pl-deezer-123456", "pl-qobuz-789" ++/// ++public static class PlaylistIdHelper ++{ ++ private const string PlaylistPrefix = "pl-"; ++ ++ /// ++ /// Checks if an ID represents an external playlist. ++ /// ++ /// The ID to check ++ /// True if the ID starts with "pl-", false otherwise ++ public static bool IsExternalPlaylist(string? id) ++ { ++ return !string.IsNullOrEmpty(id) && id.StartsWith(PlaylistPrefix, StringComparison.OrdinalIgnoreCase); ++ } ++ ++ /// ++ /// Parses a playlist ID to extract provider and external ID. ++ /// ++ /// The playlist ID in format "pl-{provider}-{externalId}" ++ /// A tuple containing (provider, externalId) ++ /// Thrown if the ID format is invalid ++ public static (string provider, string externalId) ParsePlaylistId(string id) ++ { ++ if (!IsExternalPlaylist(id)) ++ { ++ throw new ArgumentException($"Invalid playlist ID format. Expected 'pl-{{provider}}-{{externalId}}', got '{id}'", nameof(id)); ++ } ++ ++ // Remove "pl-" prefix ++ var withoutPrefix = id.Substring(PlaylistPrefix.Length); ++ ++ // Split by first dash to get provider and externalId ++ var dashIndex = withoutPrefix.IndexOf('-'); ++ if (dashIndex == -1) ++ { ++ throw new ArgumentException($"Invalid playlist ID format. Expected 'pl-{{provider}}-{{externalId}}', got '{id}'", nameof(id)); ++ } ++ ++ var provider = withoutPrefix.Substring(0, dashIndex); ++ var externalId = withoutPrefix.Substring(dashIndex + 1); ++ ++ if (string.IsNullOrEmpty(provider) || string.IsNullOrEmpty(externalId)) ++ { ++ throw new ArgumentException($"Invalid playlist ID format. Provider or external ID is empty in '{id}'", nameof(id)); ++ } ++ ++ return (provider, externalId); ++ } ++ ++ /// ++ /// Creates a playlist ID from provider and external ID. ++ /// ++ /// The provider name (e.g., "deezer", "qobuz") ++ /// The external ID from the provider ++ /// A playlist ID in format "pl-{provider}-{externalId}" ++ public static string CreatePlaylistId(string provider, string externalId) ++ { ++ if (string.IsNullOrEmpty(provider)) ++ { ++ throw new ArgumentException("Provider cannot be null or empty", nameof(provider)); ++ } ++ ++ if (string.IsNullOrEmpty(externalId)) ++ { ++ throw new ArgumentException("External ID cannot be null or empty", nameof(externalId)); ++ } ++ ++ return $"{PlaylistPrefix}{provider.ToLowerInvariant()}-{externalId}"; ++ } ++} +diff --git a/allstarr/Services/Common/RedisCacheService.cs b/allstarr/Services/Common/RedisCacheService.cs +new file mode 100644 +index 0000000..922a2d5 +--- /dev/null ++++ b/allstarr/Services/Common/RedisCacheService.cs +@@ -0,0 +1,157 @@ ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++using StackExchange.Redis; ++using System.Text.Json; ++ ++namespace allstarr.Services.Common; ++ ++/// ++/// Redis caching service for metadata and images. ++/// ++public class RedisCacheService ++{ ++ private readonly RedisSettings _settings; ++ private readonly ILogger _logger; ++ private IConnectionMultiplexer? _redis; ++ private IDatabase? _db; ++ private readonly object _lock = new(); ++ ++ public RedisCacheService( ++ IOptions settings, ++ ILogger logger) ++ { ++ _settings = settings.Value; ++ _logger = logger; ++ ++ if (_settings.Enabled) ++ { ++ InitializeConnection(); ++ } ++ } ++ ++ private void InitializeConnection() ++ { ++ try ++ { ++ _redis = ConnectionMultiplexer.Connect(_settings.ConnectionString); ++ _db = _redis.GetDatabase(); ++ _logger.LogInformation("Redis connected: {ConnectionString}", _settings.ConnectionString); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Redis connection failed. Caching disabled."); ++ _redis = null; ++ _db = null; ++ } ++ } ++ ++ public bool IsEnabled => _settings.Enabled && _db != null; ++ ++ /// ++ /// Gets a cached value as a string. ++ /// ++ public async Task GetStringAsync(string key) ++ { ++ if (!IsEnabled) return null; ++ ++ try ++ { ++ return await _db!.StringGetAsync(key); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Redis GET failed for key: {Key}", key); ++ return null; ++ } ++ } ++ ++ /// ++ /// Gets a cached value and deserializes it. ++ /// ++ public async Task GetAsync(string key) where T : class ++ { ++ var json = await GetStringAsync(key); ++ if (string.IsNullOrEmpty(json)) return null; ++ ++ try ++ { ++ return JsonSerializer.Deserialize(json); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to deserialize cached value for key: {Key}", key); ++ return null; ++ } ++ } ++ ++ /// ++ /// Sets a cached value with TTL. ++ /// ++ public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null) ++ { ++ if (!IsEnabled) return false; ++ ++ try ++ { ++ return await _db!.StringSetAsync(key, value, expiry); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Redis SET failed for key: {Key}", key); ++ return false; ++ } ++ } ++ ++ /// ++ /// Sets a cached value by serializing it with TTL. ++ /// ++ public async Task SetAsync(string key, T value, TimeSpan? expiry = null) where T : class ++ { ++ try ++ { ++ var json = JsonSerializer.Serialize(value); ++ return await SetStringAsync(key, json, expiry); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to serialize value for key: {Key}", key); ++ return false; ++ } ++ } ++ ++ /// ++ /// Deletes a cached value. ++ /// ++ public async Task DeleteAsync(string key) ++ { ++ if (!IsEnabled) return false; ++ ++ try ++ { ++ return await _db!.KeyDeleteAsync(key); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Redis DELETE failed for key: {Key}", key); ++ return false; ++ } ++ } ++ ++ /// ++ /// Checks if a key exists. ++ /// ++ public async Task ExistsAsync(string key) ++ { ++ if (!IsEnabled) return false; ++ ++ try ++ { ++ return await _db!.KeyExistsAsync(key); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Redis EXISTS failed for key: {Key}", key); ++ return false; ++ } ++ } ++} +diff --git a/allstarr/Services/Common/Result.cs b/allstarr/Services/Common/Result.cs +new file mode 100644 +index 0000000..6a5e4f5 +--- /dev/null ++++ b/allstarr/Services/Common/Result.cs +@@ -0,0 +1,99 @@ ++namespace allstarr.Services.Common; ++ ++/// ++/// Represents the result of an operation that can either succeed with a value or fail with an error. ++/// This pattern allows explicit error handling without using exceptions for control flow. ++/// ++/// The type of the value returned on success ++public class Result ++{ ++ /// ++ /// Indicates whether the operation succeeded ++ /// ++ public bool IsSuccess { get; } ++ ++ /// ++ /// Indicates whether the operation failed ++ /// ++ public bool IsFailure => !IsSuccess; ++ ++ /// ++ /// The value returned on success (null if failed) ++ /// ++ public T? Value { get; } ++ ++ /// ++ /// The error that occurred on failure (null if succeeded) ++ /// ++ public Error? Error { get; } ++ ++ private Result(bool isSuccess, T? value, Error? error) ++ { ++ IsSuccess = isSuccess; ++ Value = value; ++ Error = error; ++ } ++ ++ /// ++ /// Creates a successful result with a value ++ /// ++ public static Result Success(T value) ++ { ++ return new Result(true, value, null); ++ } ++ ++ /// ++ /// Creates a failed result with an error ++ /// ++ public static Result Failure(Error error) ++ { ++ return new Result(false, default, error); ++ } ++ ++ /// ++ /// Implicit conversion from T to Result<T> for convenience ++ /// ++ public static implicit operator Result(T value) ++ { ++ return Success(value); ++ } ++ ++ /// ++ /// Implicit conversion from Error to Result<T> for convenience ++ /// ++ public static implicit operator Result(Error error) ++ { ++ return Failure(error); ++ } ++} ++ ++/// ++/// Non-generic Result for operations that don't return a value ++/// ++public class Result ++{ ++ public bool IsSuccess { get; } ++ public bool IsFailure => !IsSuccess; ++ public Error? Error { get; } ++ ++ private Result(bool isSuccess, Error? error) ++ { ++ IsSuccess = isSuccess; ++ Error = error; ++ } ++ ++ public static Result Success() ++ { ++ return new Result(true, null); ++ } ++ ++ public static Result Failure(Error error) ++ { ++ return new Result(false, error); ++ } ++ ++ public static implicit operator Result(Error error) ++ { ++ return Failure(error); ++ } ++} +diff --git a/allstarr/Services/Deezer/DeezerDownloadService.cs b/allstarr/Services/Deezer/DeezerDownloadService.cs +new file mode 100644 +index 0000000..bf0240e +--- /dev/null ++++ b/allstarr/Services/Deezer/DeezerDownloadService.cs +@@ -0,0 +1,528 @@ ++using System.Security.Cryptography; ++using System.Text; ++using System.Text.Json; ++using Org.BouncyCastle.Crypto.Engines; ++using Org.BouncyCastle.Crypto.Modes; ++using Org.BouncyCastle.Crypto.Parameters; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using allstarr.Services.Local; ++using allstarr.Services.Common; ++using allstarr.Services.Subsonic; ++using Microsoft.Extensions.Options; ++using IOFile = System.IO.File; ++ ++namespace allstarr.Services.Deezer; ++ ++/// ++/// C# port of the DeezerDownloader JavaScript ++/// Handles Deezer authentication, track downloading and decryption ++/// ++public class DeezerDownloadService : BaseDownloadService ++{ ++ private readonly HttpClient _httpClient; ++ private readonly SemaphoreSlim _requestLock = new(1, 1); ++ ++ private readonly string? _arl; ++ private readonly string? _arlFallback; ++ private readonly string? _preferredQuality; ++ ++ private string? _apiToken; ++ private string? _licenseToken; ++ ++ private DateTime _lastRequestTime = DateTime.MinValue; ++ private readonly int _minRequestIntervalMs = 200; ++ ++ private const string DeezerApiBase = "https://api.deezer.com"; ++ ++ // Deezer's standard Blowfish CBC encryption key for track decryption ++ // This is a well-known constant used by the Deezer API, not a user-specific secret ++ private const string BfSecret = "g4el58wc0zvf9na1"; ++ ++ protected override string ProviderName => "deezer"; ++ ++ public DeezerDownloadService( ++ IHttpClientFactory httpClientFactory, ++ IConfiguration configuration, ++ ILocalLibraryService localLibraryService, ++ IMusicMetadataService metadataService, ++ IOptions subsonicSettings, ++ IOptions deezerSettings, ++ IServiceProvider serviceProvider, ++ ILogger logger) ++ : base(configuration, localLibraryService, metadataService, subsonicSettings.Value, serviceProvider, logger) ++ { ++ _httpClient = httpClientFactory.CreateClient(); ++ ++ var deezer = deezerSettings.Value; ++ _arl = deezer.Arl; ++ _arlFallback = deezer.ArlFallback; ++ _preferredQuality = deezer.Quality; ++ } ++ ++ #region BaseDownloadService Implementation ++ ++ public override async Task IsAvailableAsync() ++ { ++ if (string.IsNullOrEmpty(_arl)) ++ { ++ Logger.LogWarning("Deezer ARL not configured"); ++ return false; ++ } ++ ++ try ++ { ++ await InitializeAsync(); ++ return true; ++ } ++ catch (Exception ex) ++ { ++ Logger.LogWarning(ex, "Deezer service not available"); ++ return false; ++ } ++ } ++ ++ protected override string? ExtractExternalIdFromAlbumId(string albumId) ++ { ++ const string prefix = "ext-deezer-album-"; ++ if (albumId.StartsWith(prefix)) ++ { ++ return albumId[prefix.Length..]; ++ } ++ return null; ++ } ++ ++ protected override async Task DownloadTrackAsync(string trackId, Song song, CancellationToken cancellationToken) ++ { ++ var downloadInfo = await GetTrackDownloadInfoAsync(trackId, cancellationToken); ++ ++ Logger.LogInformation("Track token obtained for: {Title} - {Artist}", downloadInfo.Title, downloadInfo.Artist); ++ Logger.LogInformation("Using format: {Format}", downloadInfo.Format); ++ ++ // Determine extension based on format ++ var extension = downloadInfo.Format?.ToUpper() switch ++ { ++ "FLAC" => ".flac", ++ _ => ".mp3" ++ }; ++ ++ // Build organized folder structure: Artist/Album/Track using AlbumArtist (fallback to Artist for singles) ++ var artistForPath = song.AlbumArtist ?? song.Artist; ++ var basePath = SubsonicSettings.StorageMode == StorageMode.Cache ? CachePath : DownloadPath; ++ var outputPath = PathHelper.BuildTrackPath(basePath, artistForPath, song.Album, song.Title, song.Track, extension); ++ ++ // Create directories if they don't exist ++ var albumFolder = Path.GetDirectoryName(outputPath)!; ++ EnsureDirectoryExists(albumFolder); ++ ++ // Resolve unique path if file already exists ++ outputPath = PathHelper.ResolveUniquePath(outputPath); ++ ++ // Download the encrypted file ++ var response = await RetryWithBackoffAsync(async () => ++ { ++ using var request = new HttpRequestMessage(HttpMethod.Get, downloadInfo.DownloadUrl); ++ request.Headers.Add("User-Agent", "Mozilla/5.0"); ++ request.Headers.Add("Accept", "*/*"); ++ ++ return await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); ++ }); ++ ++ response.EnsureSuccessStatusCode(); ++ ++ // Download and decrypt ++ await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); ++ await using var outputFile = IOFile.Create(outputPath); ++ ++ await DecryptAndWriteStreamAsync(responseStream, outputFile, trackId, cancellationToken); ++ ++ // Close file before writing metadata ++ await outputFile.DisposeAsync(); ++ ++ // Write metadata and cover art ++ await WriteMetadataAsync(outputPath, song, cancellationToken); ++ ++ return outputPath; ++ } ++ ++ #endregion ++ ++ #region Deezer API Methods ++ ++ private async Task InitializeAsync(string? arlOverride = null) ++ { ++ var arl = arlOverride ?? _arl; ++ if (string.IsNullOrEmpty(arl)) ++ { ++ throw new Exception("ARL token required for Deezer downloads"); ++ } ++ ++ await RetryWithBackoffAsync(async () => ++ { ++ using var request = new HttpRequestMessage(HttpMethod.Post, ++ "https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token=null"); ++ ++ request.Headers.Add("Cookie", $"arl={arl}"); ++ request.Content = new StringContent("{}", Encoding.UTF8, "application/json"); ++ ++ var response = await _httpClient.SendAsync(request); ++ response.EnsureSuccessStatusCode(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var doc = JsonDocument.Parse(json); ++ ++ if (doc.RootElement.TryGetProperty("results", out var results) && ++ results.TryGetProperty("checkForm", out var checkForm)) ++ { ++ _apiToken = checkForm.GetString(); ++ ++ if (results.TryGetProperty("USER", out var user) && ++ user.TryGetProperty("OPTIONS", out var options) && ++ options.TryGetProperty("license_token", out var licenseToken)) ++ { ++ _licenseToken = licenseToken.GetString(); ++ } ++ ++ Logger.LogInformation("Deezer token refreshed successfully"); ++ return true; ++ } ++ ++ throw new Exception("Invalid ARL token"); ++ }); ++ } ++ ++ private async Task GetTrackDownloadInfoAsync(string trackId, CancellationToken cancellationToken) ++ { ++ var tryDownload = async (string arl) => ++ { ++ // Refresh token with specific ARL ++ await InitializeAsync(arl); ++ ++ return await QueueRequestAsync(async () => ++ { ++ // Get track info ++ var trackResponse = await _httpClient.GetAsync($"{DeezerApiBase}/track/{trackId}", cancellationToken); ++ trackResponse.EnsureSuccessStatusCode(); ++ ++ var trackJson = await trackResponse.Content.ReadAsStringAsync(cancellationToken); ++ var trackDoc = JsonDocument.Parse(trackJson); ++ ++ if (!trackDoc.RootElement.TryGetProperty("track_token", out var trackTokenElement)) ++ { ++ throw new Exception("Track not found or track_token missing"); ++ } ++ ++ var trackToken = trackTokenElement.GetString(); ++ var title = trackDoc.RootElement.GetProperty("title").GetString() ?? ""; ++ var artist = trackDoc.RootElement.TryGetProperty("artist", out var artistEl) ++ ? artistEl.GetProperty("name").GetString() ?? "" ++ : ""; ++ ++ // Get download URL via media API ++ // Build format list based on preferred quality ++ var formatsList = BuildFormatsList(_preferredQuality); ++ ++ var mediaRequest = new ++ { ++ license_token = _licenseToken, ++ media = new[] ++ { ++ new ++ { ++ type = "FULL", ++ formats = formatsList ++ } ++ }, ++ track_tokens = new[] { trackToken } ++ }; ++ ++ var mediaHttpRequest = new HttpRequestMessage(HttpMethod.Post, "https://media.deezer.com/v1/get_url"); ++ mediaHttpRequest.Content = new StringContent( ++ JsonSerializer.Serialize(mediaRequest), ++ Encoding.UTF8, ++ "application/json"); ++ ++ using (mediaHttpRequest) ++ { ++ var mediaResponse = await _httpClient.SendAsync(mediaHttpRequest, cancellationToken); ++ mediaResponse.EnsureSuccessStatusCode(); ++ ++ var mediaJson = await mediaResponse.Content.ReadAsStringAsync(cancellationToken); ++ var mediaDoc = JsonDocument.Parse(mediaJson); ++ ++ if (!mediaDoc.RootElement.TryGetProperty("data", out var data) || ++ data.GetArrayLength() == 0) ++ { ++ throw new Exception("No download URL available"); ++ } ++ ++ var firstData = data[0]; ++ if (!firstData.TryGetProperty("media", out var media) || ++ media.GetArrayLength() == 0) ++ { ++ throw new Exception("No media sources available - track may be unavailable in your region"); ++ } ++ ++ // Build a dictionary of available formats ++ var availableFormats = new Dictionary(); ++ foreach (var mediaItem in media.EnumerateArray()) ++ { ++ if (mediaItem.TryGetProperty("format", out var formatEl) && ++ mediaItem.TryGetProperty("sources", out var sources) && ++ sources.GetArrayLength() > 0) ++ { ++ var fmt = formatEl.GetString(); ++ var url = sources[0].GetProperty("url").GetString(); ++ if (!string.IsNullOrEmpty(fmt) && !string.IsNullOrEmpty(url)) ++ { ++ availableFormats[fmt] = url; ++ } ++ } ++ } ++ ++ if (availableFormats.Count == 0) ++ { ++ throw new Exception("No download URL found in media sources - track may be region locked"); ++ } ++ ++ // Log available formats for debugging ++ Logger.LogInformation("Available formats from Deezer: {Formats}", string.Join(", ", availableFormats.Keys)); ++ ++ // Quality priority order (highest to lowest) ++ var qualityPriority = new[] { "FLAC", "MP3_320", "MP3_128" }; ++ ++ string? selectedFormat = null; ++ string? downloadUrl = null; ++ ++ // Select the best available quality from what Deezer returned ++ foreach (var quality in qualityPriority) ++ { ++ if (availableFormats.TryGetValue(quality, out var url)) ++ { ++ selectedFormat = quality; ++ downloadUrl = url; ++ break; ++ } ++ } ++ ++ if (string.IsNullOrEmpty(downloadUrl)) ++ { ++ throw new Exception("No compatible format found in available media sources"); ++ } ++ ++ Logger.LogInformation("Selected quality: {Format}", selectedFormat); ++ ++ return new DownloadResult ++ { ++ DownloadUrl = downloadUrl, ++ Format = selectedFormat ?? "MP3_128", ++ Title = title, ++ Artist = artist ++ }; ++ } ++ }); ++ }; ++ ++ try ++ { ++ return await tryDownload(_arl!); ++ } ++ catch (Exception ex) ++ { ++ if (!string.IsNullOrEmpty(_arlFallback)) ++ { ++ Logger.LogWarning(ex, "Primary ARL failed, trying fallback ARL..."); ++ return await tryDownload(_arlFallback); ++ } ++ throw; ++ } ++ } ++ ++ #endregion ++ ++ #region Decryption ++ ++ private byte[] GetBlowfishKey(string trackId) ++ { ++ var hash = MD5.HashData(Encoding.UTF8.GetBytes(trackId)); ++ var hashHex = Convert.ToHexString(hash).ToLower(); ++ ++ var bfKey = new byte[16]; ++ for (int i = 0; i < 16; i++) ++ { ++ bfKey[i] = (byte)(hashHex[i] ^ hashHex[i + 16] ^ BfSecret[i]); ++ } ++ ++ return bfKey; ++ } ++ ++ private async Task DecryptAndWriteStreamAsync( ++ Stream input, ++ Stream output, ++ string trackId, ++ CancellationToken cancellationToken) ++ { ++ var bfKey = GetBlowfishKey(trackId); ++ var iv = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }; ++ ++ var buffer = new byte[2048]; ++ int chunkIndex = 0; ++ ++ while (true) ++ { ++ var bytesRead = await ReadExactAsync(input, buffer, cancellationToken); ++ if (bytesRead == 0) break; ++ ++ var chunk = buffer.AsSpan(0, bytesRead).ToArray(); ++ ++ // Every 3rd chunk (index % 3 == 0) is encrypted ++ if (chunkIndex % 3 == 0 && bytesRead == 2048) ++ { ++ chunk = DecryptBlowfishCbc(chunk, bfKey, iv); ++ } ++ ++ await output.WriteAsync(chunk, cancellationToken); ++ chunkIndex++; ++ } ++ } ++ ++ private async Task ReadExactAsync(Stream stream, byte[] buffer, CancellationToken cancellationToken) ++ { ++ int totalRead = 0; ++ while (totalRead < buffer.Length) ++ { ++ var bytesRead = await stream.ReadAsync(buffer.AsMemory(totalRead, buffer.Length - totalRead), cancellationToken); ++ if (bytesRead == 0) break; ++ totalRead += bytesRead; ++ } ++ return totalRead; ++ } ++ ++ private byte[] DecryptBlowfishCbc(byte[] data, byte[] key, byte[] iv) ++ { ++ // Use BouncyCastle for native Blowfish CBC decryption ++ var engine = new BlowfishEngine(); ++ var cipher = new CbcBlockCipher(engine); ++ cipher.Init(false, new ParametersWithIV(new KeyParameter(key), iv)); ++ ++ var output = new byte[data.Length]; ++ var blockSize = cipher.GetBlockSize(); // 8 bytes for Blowfish ++ ++ for (int offset = 0; offset < data.Length; offset += blockSize) ++ { ++ cipher.ProcessBlock(data, offset, output, offset); ++ } ++ ++ return output; ++ } ++ ++ #endregion ++ ++ #region Utility Methods ++ ++ /// ++ /// Builds the list of formats to request from Deezer based on preferred quality. ++ /// ++ private static object[] BuildFormatsList(string? preferredQuality) ++ { ++ var allFormats = new[] ++ { ++ new { cipher = "BF_CBC_STRIPE", format = "FLAC" }, ++ new { cipher = "BF_CBC_STRIPE", format = "MP3_320" }, ++ new { cipher = "BF_CBC_STRIPE", format = "MP3_128" } ++ }; ++ ++ if (string.IsNullOrEmpty(preferredQuality)) ++ { ++ return allFormats; ++ } ++ ++ var preferred = preferredQuality.ToUpperInvariant(); ++ ++ return preferred switch ++ { ++ "FLAC" => allFormats, ++ "MP3_320" => new object[] ++ { ++ new { cipher = "BF_CBC_STRIPE", format = "MP3_320" }, ++ new { cipher = "BF_CBC_STRIPE", format = "MP3_128" } ++ }, ++ "MP3_128" => new object[] ++ { ++ new { cipher = "BF_CBC_STRIPE", format = "MP3_128" } ++ }, ++ _ => allFormats ++ }; ++ } ++ ++ private async Task RetryWithBackoffAsync(Func> action, int maxRetries = 3, int initialDelayMs = 1000) ++ { ++ Exception? lastException = null; ++ ++ for (int attempt = 0; attempt < maxRetries; attempt++) ++ { ++ try ++ { ++ return await action(); ++ } ++ catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable || ++ ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests) ++ { ++ lastException = ex; ++ if (attempt < maxRetries - 1) ++ { ++ var delay = initialDelayMs * (int)Math.Pow(2, attempt); ++ Logger.LogWarning("Retry attempt {Attempt}/{MaxRetries} after {Delay}ms ({Message})", ++ attempt + 1, maxRetries, delay, ex.Message); ++ await Task.Delay(delay); ++ } ++ } ++ catch ++ { ++ throw; ++ } ++ } ++ ++ throw lastException!; ++ } ++ ++ private async Task RetryWithBackoffAsync(Func> action, int maxRetries = 3, int initialDelayMs = 1000) ++ { ++ await RetryWithBackoffAsync(action, maxRetries, initialDelayMs); ++ } ++ ++ private async Task QueueRequestAsync(Func> action) ++ { ++ await _requestLock.WaitAsync(); ++ try ++ { ++ var now = DateTime.UtcNow; ++ var timeSinceLastRequest = (now - _lastRequestTime).TotalMilliseconds; ++ ++ if (timeSinceLastRequest < _minRequestIntervalMs) ++ { ++ await Task.Delay((int)(_minRequestIntervalMs - timeSinceLastRequest)); ++ } ++ ++ _lastRequestTime = DateTime.UtcNow; ++ return await action(); ++ } ++ finally ++ { ++ _requestLock.Release(); ++ } ++ } ++ ++ #endregion ++ ++ private class DownloadResult ++ { ++ public string DownloadUrl { get; set; } = string.Empty; ++ public string Format { get; set; } = string.Empty; ++ public string Title { get; set; } = string.Empty; ++ public string Artist { get; set; } = string.Empty; ++ } ++} +diff --git a/allstarr/Services/Deezer/DeezerMetadataService.cs b/allstarr/Services/Deezer/DeezerMetadataService.cs +new file mode 100644 +index 0000000..ae5aa3e +--- /dev/null ++++ b/allstarr/Services/Deezer/DeezerMetadataService.cs +@@ -0,0 +1,705 @@ ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using System.Text.Json; ++using Microsoft.Extensions.Options; ++ ++namespace allstarr.Services.Deezer; ++ ++/// ++/// Metadata service implementation using the Deezer API (free, no key required) ++/// ++public class DeezerMetadataService : IMusicMetadataService ++{ ++ private readonly HttpClient _httpClient; ++ private readonly SubsonicSettings _settings; ++ private const string BaseUrl = "https://api.deezer.com"; ++ ++ public DeezerMetadataService(IHttpClientFactory httpClientFactory, IOptions settings) ++ { ++ _httpClient = httpClientFactory.CreateClient(); ++ _settings = settings.Value; ++ } ++ ++ public async Task> SearchSongsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var url = $"{BaseUrl}/search/track?q={Uri.EscapeDataString(query)}&limit={limit}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var songs = new List(); ++ if (result.RootElement.TryGetProperty("data", out var data)) ++ { ++ foreach (var track in data.EnumerateArray()) ++ { ++ var song = ParseDeezerTrack(track); ++ if (ShouldIncludeSong(song)) ++ { ++ songs.Add(song); ++ } ++ } ++ } ++ ++ return songs; ++ } ++ catch ++ { ++ return new List(); ++ } ++ } ++ ++ public async Task> SearchAlbumsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var url = $"{BaseUrl}/search/album?q={Uri.EscapeDataString(query)}&limit={limit}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var albums = new List(); ++ if (result.RootElement.TryGetProperty("data", out var data)) ++ { ++ foreach (var album in data.EnumerateArray()) ++ { ++ albums.Add(ParseDeezerAlbum(album)); ++ } ++ } ++ ++ return albums; ++ } ++ catch ++ { ++ return new List(); ++ } ++ } ++ ++ public async Task> SearchArtistsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var url = $"{BaseUrl}/search/artist?q={Uri.EscapeDataString(query)}&limit={limit}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var artists = new List(); ++ if (result.RootElement.TryGetProperty("data", out var data)) ++ { ++ foreach (var artist in data.EnumerateArray()) ++ { ++ artists.Add(ParseDeezerArtist(artist)); ++ } ++ } ++ ++ return artists; ++ } ++ catch ++ { ++ return new List(); ++ } ++ } ++ ++ public async Task SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20) ++ { ++ // Execute searches in parallel ++ var songsTask = SearchSongsAsync(query, songLimit); ++ var albumsTask = SearchAlbumsAsync(query, albumLimit); ++ var artistsTask = SearchArtistsAsync(query, artistLimit); ++ ++ await Task.WhenAll(songsTask, albumsTask, artistsTask); ++ ++ return new SearchResult ++ { ++ Songs = await songsTask, ++ Albums = await albumsTask, ++ Artists = await artistsTask ++ }; ++ } ++ ++ public async Task GetSongAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "deezer") return null; ++ ++ var url = $"{BaseUrl}/track/{externalId}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var track = JsonDocument.Parse(json).RootElement; ++ ++ if (track.TryGetProperty("error", out _)) return null; ++ ++ // For an individual track, get full metadata ++ var song = ParseDeezerTrackFull(track); ++ ++ // Get additional info from album (genre, total track count, label, copyright) ++ if (track.TryGetProperty("album", out var albumRef) && ++ albumRef.TryGetProperty("id", out var albumIdEl)) ++ { ++ var albumId = albumIdEl.GetInt64().ToString(); ++ try ++ { ++ var albumUrl = $"{BaseUrl}/album/{albumId}"; ++ var albumResponse = await _httpClient.GetAsync(albumUrl); ++ if (albumResponse.IsSuccessStatusCode) ++ { ++ var albumJson = await albumResponse.Content.ReadAsStringAsync(); ++ var albumData = JsonDocument.Parse(albumJson).RootElement; ++ ++ // Genre ++ if (albumData.TryGetProperty("genres", out var genres) && ++ genres.TryGetProperty("data", out var genresData) && ++ genresData.GetArrayLength() > 0 && ++ genresData[0].TryGetProperty("name", out var genreName)) ++ { ++ song.Genre = genreName.GetString(); ++ } ++ ++ // Total track count ++ if (albumData.TryGetProperty("nb_tracks", out var nbTracks)) ++ { ++ song.TotalTracks = nbTracks.GetInt32(); ++ } ++ ++ // Label ++ if (albumData.TryGetProperty("label", out var label)) ++ { ++ song.Label = label.GetString(); ++ } ++ ++ // Cover art XL if not already set ++ if (string.IsNullOrEmpty(song.CoverArtUrlLarge)) ++ { ++ if (albumData.TryGetProperty("cover_xl", out var coverXl)) ++ { ++ song.CoverArtUrlLarge = coverXl.GetString(); ++ } ++ else if (albumData.TryGetProperty("cover_big", out var coverBig)) ++ { ++ song.CoverArtUrlLarge = coverBig.GetString(); ++ } ++ } ++ } ++ } ++ catch ++ { ++ // If we can't get the album, continue with track info only ++ } ++ } ++ ++ return song; ++ } ++ ++ public async Task GetAlbumAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "deezer") return null; ++ ++ var url = $"{BaseUrl}/album/{externalId}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var albumElement = JsonDocument.Parse(json).RootElement; ++ ++ if (albumElement.TryGetProperty("error", out _)) return null; ++ ++ var album = ParseDeezerAlbum(albumElement); ++ ++ // Get album songs ++ if (albumElement.TryGetProperty("tracks", out var tracks) && ++ tracks.TryGetProperty("data", out var tracksData)) ++ { ++ int trackIndex = 1; ++ foreach (var track in tracksData.EnumerateArray()) ++ { ++ // Pass the album artist to ensure proper folder organization ++ var song = ParseDeezerTrack(track, trackIndex, album.Artist); ++ ++ // Ensure album metadata is set (tracks in album response may not have full album object) ++ song.Album = album.Title; ++ song.AlbumId = album.Id; ++ song.AlbumArtist = album.Artist; ++ ++ if (ShouldIncludeSong(song)) ++ { ++ album.Songs.Add(song); ++ } ++ trackIndex++; ++ } ++ } ++ ++ return album; ++ } ++ ++ public async Task GetArtistAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "deezer") return null; ++ ++ var url = $"{BaseUrl}/artist/{externalId}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var artist = JsonDocument.Parse(json).RootElement; ++ ++ if (artist.TryGetProperty("error", out _)) return null; ++ ++ return ParseDeezerArtist(artist); ++ } ++ ++ public async Task> GetArtistAlbumsAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "deezer") return new List(); ++ ++ var url = $"{BaseUrl}/artist/{externalId}/albums"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var albums = new List(); ++ if (result.RootElement.TryGetProperty("data", out var data)) ++ { ++ foreach (var album in data.EnumerateArray()) ++ { ++ albums.Add(ParseDeezerAlbum(album)); ++ } ++ } ++ ++ return albums; ++ } ++ ++ private Song ParseDeezerTrack(JsonElement track, int? fallbackTrackNumber = null, string? albumArtist = null) ++ { ++ var externalId = track.GetProperty("id").GetInt64().ToString(); ++ ++ // Try to get track_position from API, fallback to provided index ++ int? trackNumber = track.TryGetProperty("track_position", out var trackPos) ++ ? trackPos.GetInt32() ++ : fallbackTrackNumber; ++ ++ // Explicit content lyrics value ++ int? explicitContentLyrics = track.TryGetProperty("explicit_content_lyrics", out var ecl) ++ ? ecl.GetInt32() ++ : null; ++ ++ return new Song ++ { ++ Id = $"ext-deezer-song-{externalId}", ++ Title = track.GetProperty("title").GetString() ?? "", ++ Artist = track.TryGetProperty("artist", out var artist) ++ ? artist.GetProperty("name").GetString() ?? "" ++ : "", ++ ArtistId = track.TryGetProperty("artist", out var artistForId) ++ ? $"ext-deezer-artist-{artistForId.GetProperty("id").GetInt64()}" ++ : null, ++ Album = track.TryGetProperty("album", out var album) ++ ? album.GetProperty("title").GetString() ?? "" ++ : "", ++ AlbumId = track.TryGetProperty("album", out var albumForId) ++ ? $"ext-deezer-album-{albumForId.GetProperty("id").GetInt64()}" ++ : null, ++ Duration = track.TryGetProperty("duration", out var duration) ++ ? duration.GetInt32() ++ : null, ++ Track = trackNumber, ++ CoverArtUrl = track.TryGetProperty("album", out var albumForCover) && ++ albumForCover.TryGetProperty("cover_medium", out var cover) ++ ? cover.GetString() ++ : null, ++ AlbumArtist = albumArtist, ++ IsLocal = false, ++ ExternalProvider = "deezer", ++ ExternalId = externalId, ++ ExplicitContentLyrics = explicitContentLyrics ++ }; ++ } ++ ++ /// ++ /// Parses a Deezer track with all available metadata ++ /// Used for GetSongAsync which returns complete data ++ /// ++ private Song ParseDeezerTrackFull(JsonElement track) ++ { ++ var externalId = track.GetProperty("id").GetInt64().ToString(); ++ ++ // Track position et disc number ++ int? trackNumber = track.TryGetProperty("track_position", out var trackPos) ++ ? trackPos.GetInt32() ++ : null; ++ int? discNumber = track.TryGetProperty("disk_number", out var diskNum) ++ ? diskNum.GetInt32() ++ : null; ++ ++ // BPM ++ int? bpm = track.TryGetProperty("bpm", out var bpmVal) && bpmVal.ValueKind == JsonValueKind.Number ++ ? (int)bpmVal.GetDouble() ++ : null; ++ ++ // ISRC ++ string? isrc = track.TryGetProperty("isrc", out var isrcVal) ++ ? isrcVal.GetString() ++ : null; ++ ++ // Release date from album ++ string? releaseDate = null; ++ int? year = null; ++ if (track.TryGetProperty("release_date", out var relDate)) ++ { ++ releaseDate = relDate.GetString(); ++ if (!string.IsNullOrEmpty(releaseDate) && releaseDate.Length >= 4) ++ { ++ if (int.TryParse(releaseDate.Substring(0, 4), out var y)) ++ year = y; ++ } ++ } ++ else if (track.TryGetProperty("album", out var albumForDate) && ++ albumForDate.TryGetProperty("release_date", out var albumRelDate)) ++ { ++ releaseDate = albumRelDate.GetString(); ++ if (!string.IsNullOrEmpty(releaseDate) && releaseDate.Length >= 4) ++ { ++ if (int.TryParse(releaseDate.Substring(0, 4), out var y)) ++ year = y; ++ } ++ } ++ ++ // Contributors ++ var contributors = new List(); ++ if (track.TryGetProperty("contributors", out var contribs)) ++ { ++ foreach (var contrib in contribs.EnumerateArray()) ++ { ++ if (contrib.TryGetProperty("name", out var contribName)) ++ { ++ var name = contribName.GetString(); ++ if (!string.IsNullOrEmpty(name)) ++ contributors.Add(name); ++ } ++ } ++ } ++ ++ // Album artist (first artist from album, or main track artist) ++ string? albumArtist = null; ++ if (track.TryGetProperty("album", out var albumForArtist) && ++ albumForArtist.TryGetProperty("artist", out var albumArtistEl)) ++ { ++ albumArtist = albumArtistEl.TryGetProperty("name", out var aName) ++ ? aName.GetString() ++ : null; ++ } ++ ++ // Cover art URLs (different sizes) ++ string? coverMedium = null; ++ string? coverLarge = null; ++ if (track.TryGetProperty("album", out var albumForCover)) ++ { ++ coverMedium = albumForCover.TryGetProperty("cover_medium", out var cm) ++ ? cm.GetString() ++ : null; ++ coverLarge = albumForCover.TryGetProperty("cover_xl", out var cxl) ++ ? cxl.GetString() ++ : (albumForCover.TryGetProperty("cover_big", out var cb) ? cb.GetString() : null); ++ } ++ ++ // Explicit content lyrics value ++ int? explicitContentLyrics = track.TryGetProperty("explicit_content_lyrics", out var ecl) ++ ? ecl.GetInt32() ++ : null; ++ ++ return new Song ++ { ++ Id = $"ext-deezer-song-{externalId}", ++ Title = track.GetProperty("title").GetString() ?? "", ++ Artist = track.TryGetProperty("artist", out var artist) ++ ? artist.GetProperty("name").GetString() ?? "" ++ : "", ++ ArtistId = track.TryGetProperty("artist", out var artistForId) ++ ? $"ext-deezer-artist-{artistForId.GetProperty("id").GetInt64()}" ++ : null, ++ Album = track.TryGetProperty("album", out var album) ++ ? album.GetProperty("title").GetString() ?? "" ++ : "", ++ AlbumId = track.TryGetProperty("album", out var albumForId) ++ ? $"ext-deezer-album-{albumForId.GetProperty("id").GetInt64()}" ++ : null, ++ Duration = track.TryGetProperty("duration", out var duration) ++ ? duration.GetInt32() ++ : null, ++ Track = trackNumber, ++ DiscNumber = discNumber, ++ Year = year, ++ Bpm = bpm, ++ Isrc = isrc, ++ ReleaseDate = releaseDate, ++ AlbumArtist = albumArtist, ++ Contributors = contributors, ++ CoverArtUrl = coverMedium, ++ CoverArtUrlLarge = coverLarge, ++ IsLocal = false, ++ ExternalProvider = "deezer", ++ ExternalId = externalId, ++ ExplicitContentLyrics = explicitContentLyrics ++ }; ++ } ++ ++ private Album ParseDeezerAlbum(JsonElement album) ++ { ++ var externalId = album.GetProperty("id").GetInt64().ToString(); ++ ++ return new Album ++ { ++ Id = $"ext-deezer-album-{externalId}", ++ Title = album.GetProperty("title").GetString() ?? "", ++ Artist = album.TryGetProperty("artist", out var artist) ++ ? artist.GetProperty("name").GetString() ?? "" ++ : "", ++ ArtistId = album.TryGetProperty("artist", out var artistForId) ++ ? $"ext-deezer-artist-{artistForId.GetProperty("id").GetInt64()}" ++ : null, ++ Year = album.TryGetProperty("release_date", out var releaseDate) ++ ? int.TryParse(releaseDate.GetString()?.Split('-')[0], out var year) ? year : null ++ : null, ++ SongCount = album.TryGetProperty("nb_tracks", out var nbTracks) ++ ? nbTracks.GetInt32() ++ : null, ++ CoverArtUrl = album.TryGetProperty("cover_medium", out var cover) ++ ? cover.GetString() ++ : null, ++ Genre = album.TryGetProperty("genres", out var genres) && ++ genres.TryGetProperty("data", out var genresData) && ++ genresData.GetArrayLength() > 0 ++ ? genresData[0].GetProperty("name").GetString() ++ : null, ++ IsLocal = false, ++ ExternalProvider = "deezer", ++ ExternalId = externalId ++ }; ++ } ++ ++ private Artist ParseDeezerArtist(JsonElement artist) ++ { ++ var externalId = artist.GetProperty("id").GetInt64().ToString(); ++ ++ return new Artist ++ { ++ Id = $"ext-deezer-artist-{externalId}", ++ Name = artist.GetProperty("name").GetString() ?? "", ++ ImageUrl = artist.TryGetProperty("picture_medium", out var picture) ++ ? picture.GetString() ++ : null, ++ AlbumCount = artist.TryGetProperty("nb_album", out var nbAlbum) ++ ? nbAlbum.GetInt32() ++ : null, ++ IsLocal = false, ++ ExternalProvider = "deezer", ++ ExternalId = externalId ++ }; ++ } ++ ++ public async Task> SearchPlaylistsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var url = $"{BaseUrl}/search/playlist?q={Uri.EscapeDataString(query)}&limit={limit}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var playlists = new List(); ++ if (result.RootElement.TryGetProperty("data", out var data)) ++ { ++ foreach (var playlist in data.EnumerateArray()) ++ { ++ playlists.Add(ParseDeezerPlaylist(playlist)); ++ } ++ } ++ ++ return playlists; ++ } ++ catch ++ { ++ return new List(); ++ } ++ } ++ ++ public async Task GetPlaylistAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "deezer") return null; ++ ++ try ++ { ++ var url = $"{BaseUrl}/playlist/{externalId}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var playlistElement = JsonDocument.Parse(json).RootElement; ++ ++ if (playlistElement.TryGetProperty("error", out _)) return null; ++ ++ return ParseDeezerPlaylist(playlistElement); ++ } ++ catch ++ { ++ return null; ++ } ++ } ++ ++ public async Task> GetPlaylistTracksAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "deezer") return new List(); ++ ++ try ++ { ++ var url = $"{BaseUrl}/playlist/{externalId}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var playlistElement = JsonDocument.Parse(json).RootElement; ++ ++ if (playlistElement.TryGetProperty("error", out _)) return new List(); ++ ++ var songs = new List(); ++ ++ // Get playlist name for album field ++ var playlistName = playlistElement.TryGetProperty("title", out var titleEl) ++ ? titleEl.GetString() ?? "Unknown Playlist" ++ : "Unknown Playlist"; ++ ++ if (playlistElement.TryGetProperty("tracks", out var tracks) && ++ tracks.TryGetProperty("data", out var tracksData)) ++ { ++ int trackIndex = 1; ++ foreach (var track in tracksData.EnumerateArray()) ++ { ++ // For playlists, use the track's own artist (not a single album artist) ++ var song = ParseDeezerTrack(track, trackIndex); ++ ++ // Override album name to be the playlist name ++ song.Album = playlistName; ++ ++ if (ShouldIncludeSong(song)) ++ { ++ songs.Add(song); ++ } ++ trackIndex++; ++ } ++ } ++ ++ return songs; ++ } ++ catch ++ { ++ return new List(); ++ } ++ } ++ ++ private ExternalPlaylist ParseDeezerPlaylist(JsonElement playlist) ++ { ++ var externalId = playlist.GetProperty("id").GetInt64().ToString(); ++ ++ // Get curator/creator name ++ string? curatorName = null; ++ if (playlist.TryGetProperty("user", out var user) && ++ user.TryGetProperty("name", out var userName)) ++ { ++ curatorName = userName.GetString(); ++ } ++ else if (playlist.TryGetProperty("creator", out var creator) && ++ creator.TryGetProperty("name", out var creatorName)) ++ { ++ curatorName = creatorName.GetString(); ++ } ++ ++ // Get creation date ++ DateTime? createdDate = null; ++ if (playlist.TryGetProperty("creation_date", out var creationDateEl)) ++ { ++ var dateStr = creationDateEl.GetString(); ++ if (!string.IsNullOrEmpty(dateStr) && DateTime.TryParse(dateStr, out var date)) ++ { ++ createdDate = date; ++ } ++ } ++ ++ return new ExternalPlaylist ++ { ++ Id = Common.PlaylistIdHelper.CreatePlaylistId("deezer", externalId), ++ Name = playlist.GetProperty("title").GetString() ?? "", ++ Description = playlist.TryGetProperty("description", out var desc) ++ ? desc.GetString() ++ : null, ++ CuratorName = curatorName, ++ Provider = "deezer", ++ ExternalId = externalId, ++ TrackCount = playlist.TryGetProperty("nb_tracks", out var nbTracks) ++ ? nbTracks.GetInt32() ++ : 0, ++ Duration = playlist.TryGetProperty("duration", out var duration) ++ ? duration.GetInt32() ++ : 0, ++ CoverUrl = playlist.TryGetProperty("picture_medium", out var picture) ++ ? picture.GetString() ++ : (playlist.TryGetProperty("picture_big", out var pictureBig) ++ ? pictureBig.GetString() ++ : null), ++ CreatedDate = createdDate ++ }; ++ } ++ ++ /// ++ /// Determines whether a song should be included based on the explicit content filter setting ++ /// ++ /// The song to check ++ /// True if the song should be included, false otherwise ++ private bool ShouldIncludeSong(Song song) ++ { ++ // If no explicit content info, include the song ++ if (song.ExplicitContentLyrics == null) ++ return true; ++ ++ return _settings.ExplicitFilter switch ++ { ++ // All: No filtering, include everything ++ ExplicitFilter.All => true, ++ ++ // ExplicitOnly: Exclude clean/edited versions (value 3) ++ // Include: 0 (naturally clean), 1 (explicit), 2 (not applicable), 6/7 (unknown) ++ ExplicitFilter.ExplicitOnly => song.ExplicitContentLyrics != 3, ++ ++ // CleanOnly: Only show clean content ++ // Include: 0 (naturally clean), 3 (clean/edited version) ++ // Exclude: 1 (explicit) ++ ExplicitFilter.CleanOnly => song.ExplicitContentLyrics != 1, ++ ++ _ => true ++ }; ++ } ++} +diff --git a/allstarr/Services/Deezer/DeezerStartupValidator.cs b/allstarr/Services/Deezer/DeezerStartupValidator.cs +new file mode 100644 +index 0000000..91d6f3a +--- /dev/null ++++ b/allstarr/Services/Deezer/DeezerStartupValidator.cs +@@ -0,0 +1,157 @@ ++using System.Text; ++using System.Text.Json; ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++using allstarr.Services.Validation; ++ ++namespace allstarr.Services.Deezer; ++ ++/// ++/// Validates Deezer ARL credentials at startup ++/// ++public class DeezerStartupValidator : BaseStartupValidator ++{ ++ private readonly DeezerSettings _settings; ++ ++ public override string ServiceName => "Deezer"; ++ ++ public DeezerStartupValidator(IOptions settings, HttpClient httpClient) ++ : base(httpClient) ++ { ++ _settings = settings.Value; ++ } ++ ++ public override async Task ValidateAsync(CancellationToken cancellationToken) ++ { ++ var arl = _settings.Arl; ++ var arlFallback = _settings.ArlFallback; ++ var quality = _settings.Quality; ++ ++ Console.WriteLine(); ++ ++ if (string.IsNullOrWhiteSpace(arl)) ++ { ++ WriteStatus("Deezer ARL", "NOT CONFIGURED", ConsoleColor.Red); ++ WriteDetail("Set the Deezer__Arl environment variable"); ++ return ValidationResult.NotConfigured("Deezer ARL not configured"); ++ } ++ ++ WriteStatus("Deezer ARL", MaskSecret(arl), ConsoleColor.Cyan); ++ ++ if (!string.IsNullOrWhiteSpace(arlFallback)) ++ { ++ WriteStatus("Deezer ARL Fallback", MaskSecret(arlFallback), ConsoleColor.Cyan); ++ } ++ ++ WriteStatus("Deezer Quality", string.IsNullOrWhiteSpace(quality) ? "auto (highest available)" : quality, ConsoleColor.Cyan); ++ ++ // Validate ARL by calling Deezer API ++ await ValidateArlTokenAsync(arl, "primary", cancellationToken); ++ ++ if (!string.IsNullOrWhiteSpace(arlFallback)) ++ { ++ await ValidateArlTokenAsync(arlFallback, "fallback", cancellationToken); ++ } ++ ++ return ValidationResult.Success("Deezer validation completed"); ++ } ++ ++ private async Task ValidateArlTokenAsync(string arl, string label, CancellationToken cancellationToken) ++ { ++ var fieldName = $"Deezer ARL ({label})"; ++ ++ try ++ { ++ using var request = new HttpRequestMessage(HttpMethod.Post, ++ "https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token=null"); ++ ++ request.Headers.Add("Cookie", $"arl={arl}"); ++ request.Content = new StringContent("{}", Encoding.UTF8, "application/json"); ++ ++ var response = await _httpClient.SendAsync(request, cancellationToken); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ WriteStatus(fieldName, $"HTTP {(int)response.StatusCode}", ConsoleColor.Red); ++ return; ++ } ++ ++ var json = await response.Content.ReadAsStringAsync(cancellationToken); ++ var doc = JsonDocument.Parse(json); ++ ++ if (doc.RootElement.TryGetProperty("results", out var results) && ++ results.TryGetProperty("USER", out var user)) ++ { ++ if (user.TryGetProperty("USER_ID", out var userId)) ++ { ++ var userIdValue = userId.ValueKind == JsonValueKind.Number ++ ? userId.GetInt64() ++ : long.TryParse(userId.GetString(), out var parsed) ? parsed : 0; ++ ++ if (userIdValue > 0) ++ { ++ // BLOG_NAME is the username displayed on Deezer ++ var userName = user.TryGetProperty("BLOG_NAME", out var blogName) && blogName.GetString() is string bn && !string.IsNullOrEmpty(bn) ++ ? bn ++ : user.TryGetProperty("NAME", out var name) && name.GetString() is string n && !string.IsNullOrEmpty(n) ++ ? n ++ : "Unknown"; ++ ++ var offerName = GetOfferName(user); ++ ++ WriteStatus(fieldName, "VALID", ConsoleColor.Green); ++ WriteDetail($"Logged in as {userName} ({offerName})"); ++ return; ++ } ++ } ++ ++ WriteStatus(fieldName, "INVALID", ConsoleColor.Red); ++ WriteDetail("Token is expired or invalid"); ++ } ++ else ++ { ++ WriteStatus(fieldName, "INVALID", ConsoleColor.Red); ++ WriteDetail("Unexpected response from Deezer"); ++ } ++ } ++ catch (TaskCanceledException) ++ { ++ WriteStatus(fieldName, "TIMEOUT", ConsoleColor.Yellow); ++ WriteDetail("Could not reach Deezer within 10 seconds"); ++ } ++ catch (HttpRequestException ex) ++ { ++ WriteStatus(fieldName, "UNREACHABLE", ConsoleColor.Yellow); ++ WriteDetail(ex.Message); ++ } ++ catch (Exception ex) ++ { ++ WriteStatus(fieldName, "ERROR", ConsoleColor.Red); ++ WriteDetail(ex.Message); ++ } ++ } ++ ++ private static string GetOfferName(JsonElement user) ++ { ++ if (!user.TryGetProperty("OPTIONS", out var options)) ++ { ++ return "Free"; ++ } ++ ++ // Check actual streaming capabilities, not just license_token presence ++ var hasLossless = options.TryGetProperty("web_lossless", out var webLossless) && webLossless.GetBoolean(); ++ var hasHq = options.TryGetProperty("web_hq", out var webHq) && webHq.GetBoolean(); ++ ++ if (hasLossless) ++ { ++ return "Premium+ (Lossless)"; ++ } ++ ++ if (hasHq) ++ { ++ return "Premium (HQ)"; ++ } ++ ++ return "Free"; ++ } ++} +diff --git a/allstarr/Services/IDownloadService.cs b/allstarr/Services/IDownloadService.cs +new file mode 100644 +index 0000000..1f95f76 +--- /dev/null ++++ b/allstarr/Services/IDownloadService.cs +@@ -0,0 +1,57 @@ ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++ ++namespace allstarr.Services; ++ ++/// ++/// Interface for the music download service (Deezspot or other) ++/// ++public interface IDownloadService ++{ ++ /// ++ /// Downloads a song from an external provider ++ /// ++ /// The provider (deezer, spotify) ++ /// The ID on the external provider ++ /// Cancellation token ++ /// The path to the downloaded file ++ Task DownloadSongAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default); ++ ++ /// ++ /// Downloads a song and streams the result progressively ++ /// ++ /// The provider (deezer, spotify) ++ /// The ID on the external provider ++ /// Cancellation token ++ /// A stream of the audio file ++ Task DownloadAndStreamAsync(string externalProvider, string externalId, CancellationToken cancellationToken = default); ++ ++ /// ++ /// Downloads remaining tracks from an album in background (excluding the specified track) ++ /// ++ /// The provider (deezer, spotify) ++ /// The album ID on the external provider ++ /// The track ID to exclude (already downloaded) ++ void DownloadRemainingAlbumTracksInBackground(string externalProvider, string albumExternalId, string excludeTrackExternalId); ++ ++ /// ++ /// Checks if a song is currently being downloaded ++ /// ++ DownloadInfo? GetDownloadStatus(string songId); ++ ++ /// ++ /// Gets the local path for a song if it has been downloaded already ++ /// ++ /// The provider (deezer, qobuz, etc.) ++ /// The ID on the external provider ++ /// The local file path if exists, null otherwise ++ Task GetLocalPathIfExistsAsync(string externalProvider, string externalId); ++ ++ /// ++ /// Checks if the service is properly configured and functional ++ /// ++ Task IsAvailableAsync(); ++} +diff --git a/allstarr/Services/IMusicMetadataService.cs b/allstarr/Services/IMusicMetadataService.cs +new file mode 100644 +index 0000000..d32f715 +--- /dev/null ++++ b/allstarr/Services/IMusicMetadataService.cs +@@ -0,0 +1,81 @@ ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++ ++namespace allstarr.Services; ++ ++/// ++/// Interface for external music metadata search service ++/// (Deezer API, Spotify API, MusicBrainz, etc.) ++/// ++public interface IMusicMetadataService ++{ ++ /// ++ /// Searches for songs on external providers ++ /// ++ /// Search term ++ /// Maximum number of results ++ /// List of found songs ++ Task> SearchSongsAsync(string query, int limit = 20); ++ ++ /// ++ /// Searches for albums on external providers ++ /// ++ Task> SearchAlbumsAsync(string query, int limit = 20); ++ ++ /// ++ /// Searches for artists on external providers ++ /// ++ Task> SearchArtistsAsync(string query, int limit = 20); ++ ++ /// ++ /// Combined search (songs, albums, artists) ++ /// ++ Task SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20); ++ ++ /// ++ /// Gets details of an external song ++ /// ++ Task GetSongAsync(string externalProvider, string externalId); ++ ++ /// ++ /// Gets details of an external album with its songs ++ /// ++ Task GetAlbumAsync(string externalProvider, string externalId); ++ ++ /// ++ /// Gets details of an external artist ++ /// ++ Task GetArtistAsync(string externalProvider, string externalId); ++ ++ /// ++ /// Gets an artist's albums ++ /// ++ Task> GetArtistAlbumsAsync(string externalProvider, string externalId); ++ ++ /// ++ /// Searches for playlists on external providers ++ /// ++ /// Search term ++ /// Maximum number of results ++ /// List of found playlists ++ Task> SearchPlaylistsAsync(string query, int limit = 20); ++ ++ /// ++ /// Gets details of an external playlist (metadata only, not tracks) ++ /// ++ /// Provider name (e.g., "deezer", "qobuz") ++ /// Playlist ID from the provider ++ /// Playlist details or null if not found ++ Task GetPlaylistAsync(string externalProvider, string externalId); ++ ++ /// ++ /// Gets all tracks from an external playlist ++ /// ++ /// Provider name (e.g., "deezer", "qobuz") ++ /// Playlist ID from the provider ++ /// List of songs in the playlist ++ Task> GetPlaylistTracksAsync(string externalProvider, string externalId); ++} +diff --git a/allstarr/Services/Jellyfin/JellyfinModelMapper.cs b/allstarr/Services/Jellyfin/JellyfinModelMapper.cs +new file mode 100644 +index 0000000..6ea1502 +--- /dev/null ++++ b/allstarr/Services/Jellyfin/JellyfinModelMapper.cs +@@ -0,0 +1,385 @@ ++using System.Text.Json; ++using allstarr.Models.Domain; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++ ++namespace allstarr.Services.Jellyfin; ++ ++/// ++/// Maps between Jellyfin API responses and domain models. ++/// ++public class JellyfinModelMapper ++{ ++ private readonly JellyfinResponseBuilder _responseBuilder; ++ private readonly ILogger _logger; ++ ++ public JellyfinModelMapper( ++ JellyfinResponseBuilder responseBuilder, ++ ILogger logger) ++ { ++ _responseBuilder = responseBuilder; ++ _logger = logger; ++ } ++ ++ /// ++ /// Parses a Jellyfin items response into domain objects. ++ /// ++ public (List Songs, List Albums, List Artists) ParseItemsResponse(JsonDocument? response) ++ { ++ var songs = new List(); ++ var albums = new List(); ++ var artists = new List(); ++ ++ if (response == null) ++ { ++ return (songs, albums, artists); ++ } ++ ++ try ++ { ++ JsonElement items; ++ ++ // Handle both direct array and Items property ++ if (response.RootElement.TryGetProperty("Items", out items)) ++ { ++ // Standard items response ++ } ++ else if (response.RootElement.ValueKind == JsonValueKind.Array) ++ { ++ items = response.RootElement; ++ } ++ else ++ { ++ return (songs, albums, artists); ++ } ++ ++ foreach (var item in items.EnumerateArray()) ++ { ++ var type = item.TryGetProperty("Type", out var typeEl) ++ ? typeEl.GetString() ++ : null; ++ ++ switch (type) ++ { ++ case "Audio": ++ songs.Add(ParseSong(item)); ++ break; ++ case "MusicAlbum": ++ albums.Add(ParseAlbum(item)); ++ break; ++ case "MusicArtist": ++ case "Artist": ++ artists.Add(ParseArtist(item)); ++ break; ++ } ++ } ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Error parsing Jellyfin items response"); ++ } ++ ++ return (songs, albums, artists); ++ } ++ ++ /// ++ /// Parses a Jellyfin search hints response. ++ /// ++ public (List Songs, List Albums, List Artists) ParseSearchHintsResponse(JsonDocument? response) ++ { ++ var songs = new List(); ++ var albums = new List(); ++ var artists = new List(); ++ ++ if (response == null) ++ { ++ return (songs, albums, artists); ++ } ++ ++ try ++ { ++ if (!response.RootElement.TryGetProperty("SearchHints", out var hints)) ++ { ++ return (songs, albums, artists); ++ } ++ ++ foreach (var hint in hints.EnumerateArray()) ++ { ++ var type = hint.TryGetProperty("Type", out var typeEl) ++ ? typeEl.GetString() ++ : null; ++ ++ switch (type) ++ { ++ case "Audio": ++ songs.Add(ParseSongFromHint(hint)); ++ break; ++ case "MusicAlbum": ++ albums.Add(ParseAlbumFromHint(hint)); ++ break; ++ case "MusicArtist": ++ case "Artist": ++ artists.Add(ParseArtistFromHint(hint)); ++ break; ++ } ++ } ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Error parsing Jellyfin search hints response"); ++ } ++ ++ return (songs, albums, artists); ++ } ++ ++ /// ++ /// Parses a single Jellyfin item as a Song. ++ /// ++ public Song ParseSong(JsonElement item) ++ { ++ var id = item.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; ++ var runTimeTicks = item.TryGetProperty("RunTimeTicks", out var rtt) ? rtt.GetInt64() : 0; ++ ++ var song = new Song ++ { ++ Id = id, ++ Title = item.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", ++ Album = item.TryGetProperty("Album", out var album) ? album.GetString() ?? "" : "", ++ AlbumId = item.TryGetProperty("AlbumId", out var albumId) ? albumId.GetString() : null, ++ Duration = (int)(runTimeTicks / TimeSpan.TicksPerSecond), ++ Track = item.TryGetProperty("IndexNumber", out var track) ? track.GetInt32() : null, ++ DiscNumber = item.TryGetProperty("ParentIndexNumber", out var disc) ? disc.GetInt32() : null, ++ Year = item.TryGetProperty("ProductionYear", out var year) ? year.GetInt32() : null, ++ IsLocal = true ++ }; ++ ++ // Get artist info ++ if (item.TryGetProperty("Artists", out var artists) && artists.GetArrayLength() > 0) ++ { ++ song.Artist = artists[0].GetString() ?? ""; ++ } ++ else if (item.TryGetProperty("AlbumArtist", out var albumArtist)) ++ { ++ song.Artist = albumArtist.GetString() ?? ""; ++ } ++ ++ if (item.TryGetProperty("ArtistItems", out var artistItems) && artistItems.GetArrayLength() > 0) ++ { ++ var firstArtist = artistItems[0]; ++ song.ArtistId = firstArtist.TryGetProperty("Id", out var artId) ? artId.GetString() : null; ++ } ++ ++ // Get genre ++ if (item.TryGetProperty("Genres", out var genres) && genres.GetArrayLength() > 0) ++ { ++ song.Genre = genres[0].GetString(); ++ } ++ ++ // Get provider IDs ++ if (item.TryGetProperty("ProviderIds", out var providerIds)) ++ { ++ if (providerIds.TryGetProperty("ISRC", out var isrc)) ++ { ++ song.Isrc = isrc.GetString(); ++ } ++ } ++ ++ // Cover art URL construction ++ song.CoverArtUrl = $"/Items/{id}/Images/Primary"; ++ ++ return song; ++ } ++ ++ /// ++ /// Parses a search hint as a Song. ++ /// ++ private Song ParseSongFromHint(JsonElement hint) ++ { ++ var id = hint.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; ++ var runTimeTicks = hint.TryGetProperty("RunTimeTicks", out var rtt) ? rtt.GetInt64() : 0; ++ ++ return new Song ++ { ++ Id = id, ++ Title = hint.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", ++ Album = hint.TryGetProperty("Album", out var album) ? album.GetString() ?? "" : "", ++ Artist = hint.TryGetProperty("AlbumArtist", out var artist) ? artist.GetString() ?? "" : "", ++ Duration = (int)(runTimeTicks / TimeSpan.TicksPerSecond), ++ IsLocal = true, ++ CoverArtUrl = $"/Items/{id}/Images/Primary" ++ }; ++ } ++ ++ /// ++ /// Parses a single Jellyfin item as an Album. ++ /// ++ public Album ParseAlbum(JsonElement item) ++ { ++ var id = item.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; ++ ++ var album = new Album ++ { ++ Id = id, ++ Title = item.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", ++ Artist = item.TryGetProperty("AlbumArtist", out var artist) ? artist.GetString() ?? "" : "", ++ Year = item.TryGetProperty("ProductionYear", out var year) ? year.GetInt32() : null, ++ SongCount = item.TryGetProperty("ChildCount", out var count) ? count.GetInt32() : null, ++ IsLocal = true, ++ CoverArtUrl = $"/Items/{id}/Images/Primary" ++ }; ++ ++ // Get artist ID ++ if (item.TryGetProperty("AlbumArtists", out var albumArtists) && albumArtists.GetArrayLength() > 0) ++ { ++ var firstArtist = albumArtists[0]; ++ album.ArtistId = firstArtist.TryGetProperty("Id", out var artId) ? artId.GetString() : null; ++ } ++ ++ // Get genre ++ if (item.TryGetProperty("Genres", out var genres) && genres.GetArrayLength() > 0) ++ { ++ album.Genre = genres[0].GetString(); ++ } ++ ++ return album; ++ } ++ ++ /// ++ /// Parses a search hint as an Album. ++ /// ++ private Album ParseAlbumFromHint(JsonElement hint) ++ { ++ var id = hint.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; ++ ++ return new Album ++ { ++ Id = id, ++ Title = hint.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", ++ Artist = hint.TryGetProperty("AlbumArtist", out var artist) ? artist.GetString() ?? "" : "", ++ Year = hint.TryGetProperty("ProductionYear", out var year) ? year.GetInt32() : null, ++ IsLocal = true, ++ CoverArtUrl = $"/Items/{id}/Images/Primary" ++ }; ++ } ++ ++ /// ++ /// Parses a single Jellyfin item as an Artist. ++ /// ++ public Artist ParseArtist(JsonElement item) ++ { ++ var id = item.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; ++ ++ return new Artist ++ { ++ Id = id, ++ Name = item.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", ++ AlbumCount = item.TryGetProperty("AlbumCount", out var count) ? count.GetInt32() : null, ++ IsLocal = true, ++ ImageUrl = $"/Items/{id}/Images/Primary" ++ }; ++ } ++ ++ /// ++ /// Parses a search hint as an Artist. ++ /// ++ private Artist ParseArtistFromHint(JsonElement hint) ++ { ++ var id = hint.TryGetProperty("Id", out var idEl) ? idEl.GetString() ?? "" : ""; ++ ++ return new Artist ++ { ++ Id = id, ++ Name = hint.TryGetProperty("Name", out var name) ? name.GetString() ?? "" : "", ++ IsLocal = true, ++ ImageUrl = $"/Items/{id}/Images/Primary" ++ }; ++ } ++ ++ /// ++ /// Merges local Jellyfin results with external search results. ++ /// ++ public (List> MergedSongs, ++ List> MergedAlbums, ++ List> MergedArtists) MergeSearchResults( ++ List localSongs, ++ List localAlbums, ++ List localArtists, ++ SearchResult externalResult, ++ List externalPlaylists) ++ { ++ // Convert local results to Jellyfin format ++ var mergedSongs = localSongs ++ .Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)) ++ .Concat(externalResult.Songs.Select(s => _responseBuilder.ConvertSongToJellyfinItem(s))) ++ .ToList(); ++ ++ // Merge albums with playlists ++ var mergedAlbums = localAlbums ++ .Select(a => _responseBuilder.ConvertAlbumToJellyfinItem(a)) ++ .Concat(externalResult.Albums.Select(a => _responseBuilder.ConvertAlbumToJellyfinItem(a))) ++ .Concat(externalPlaylists.Select(p => _responseBuilder.ConvertPlaylistToAlbumItem(p))) ++ .ToList(); ++ ++ // Deduplicate artists by name - prefer local artists ++ var localArtistNames = new HashSet( ++ localArtists.Select(a => a.Name), ++ StringComparer.OrdinalIgnoreCase); ++ ++ var mergedArtists = localArtists ++ .Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)) ++ .ToList(); ++ ++ foreach (var externalArtist in externalResult.Artists) ++ { ++ if (!localArtistNames.Contains(externalArtist.Name)) ++ { ++ mergedArtists.Add(_responseBuilder.ConvertArtistToJellyfinItem(externalArtist)); ++ } ++ } ++ ++ return (mergedSongs, mergedAlbums, mergedArtists); ++ } ++ ++ /// ++ /// Parses an album with its tracks from a Jellyfin response. ++ /// ++ public Album? ParseAlbumWithTracks(JsonDocument? albumResponse, JsonDocument? tracksResponse) ++ { ++ if (albumResponse == null) ++ { ++ return null; ++ } ++ ++ var album = ParseAlbum(albumResponse.RootElement); ++ ++ if (tracksResponse != null && tracksResponse.RootElement.TryGetProperty("Items", out var tracks)) ++ { ++ foreach (var track in tracks.EnumerateArray()) ++ { ++ album.Songs.Add(ParseSong(track)); ++ } ++ } ++ ++ return album; ++ } ++ ++ /// ++ /// Parses an artist with albums from Jellyfin responses. ++ /// ++ public Artist? ParseArtistWithAlbums(JsonDocument? artistResponse, JsonDocument? albumsResponse) ++ { ++ if (artistResponse == null) ++ { ++ return null; ++ } ++ ++ var artist = ParseArtist(artistResponse.RootElement); ++ ++ if (albumsResponse != null && albumsResponse.RootElement.TryGetProperty("Items", out var albums)) ++ { ++ artist.AlbumCount = albums.GetArrayLength(); ++ } ++ ++ return artist; ++ } ++} +diff --git a/allstarr/Services/Jellyfin/JellyfinProxyService.cs b/allstarr/Services/Jellyfin/JellyfinProxyService.cs +new file mode 100644 +index 0000000..75df0fb +--- /dev/null ++++ b/allstarr/Services/Jellyfin/JellyfinProxyService.cs +@@ -0,0 +1,698 @@ ++using Microsoft.AspNetCore.Mvc; ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++using allstarr.Services.Common; ++using System.Net.Http.Headers; ++using System.Text.Json; ++ ++namespace allstarr.Services.Jellyfin; ++ ++/// ++/// Handles proxying requests to the Jellyfin server and authentication. ++/// ++public class JellyfinProxyService ++{ ++ private readonly HttpClient _httpClient; ++ private readonly JellyfinSettings _settings; ++ private readonly IHttpContextAccessor _httpContextAccessor; ++ private readonly ILogger _logger; ++ private readonly RedisCacheService _cache; ++ private string? _cachedMusicLibraryId; ++ private bool _libraryIdDetected = false; ++ ++ // Expose HttpClient for direct streaming scenarios ++ public HttpClient HttpClient => _httpClient; ++ ++ public JellyfinProxyService( ++ IHttpClientFactory httpClientFactory, ++ IOptions settings, ++ IHttpContextAccessor httpContextAccessor, ++ ILogger logger, ++ RedisCacheService cache) ++ { ++ _httpClient = httpClientFactory.CreateClient(); ++ _settings = settings.Value; ++ _httpContextAccessor = httpContextAccessor; ++ _logger = logger; ++ _cache = cache; ++ } ++ ++ /// ++ /// Gets the music library ID, auto-detecting it if not configured. ++ /// ++ private async Task GetMusicLibraryIdAsync() ++ { ++ // Return configured library ID if set ++ if (!string.IsNullOrEmpty(_settings.LibraryId)) ++ { ++ return _settings.LibraryId; ++ } ++ ++ // Return cached value if already detected ++ if (_libraryIdDetected) ++ { ++ return _cachedMusicLibraryId; ++ } ++ ++ // Auto-detect music library ID ++ try ++ { ++ _logger.LogInformation("Auto-detecting music library ID..."); ++ _cachedMusicLibraryId = await GetMusicLibraryIdInternalAsync(); ++ _libraryIdDetected = true; ++ ++ if (!string.IsNullOrEmpty(_cachedMusicLibraryId)) ++ { ++ _logger.LogInformation("Music library auto-detected: {LibraryId}", _cachedMusicLibraryId); ++ } ++ else ++ { ++ _logger.LogWarning("Could not auto-detect music library. All content types will be visible. Set JELLYFIN_LIBRARY_ID to filter to music only."); ++ } ++ ++ return _cachedMusicLibraryId; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to auto-detect music library ID"); ++ _libraryIdDetected = true; // Don't keep trying ++ return null; ++ } ++ } ++ ++ /// ++ /// Public method for controllers to get the music library ID for filtering. ++ /// ++ public async Task GetMusicLibraryIdForFilteringAsync() ++ { ++ return await GetMusicLibraryIdAsync(); ++ } ++ ++ /// ++ /// Gets the authorization header value for Jellyfin API requests. ++ /// ++ private string GetAuthorizationHeader() ++ { ++ return $"MediaBrowser Client=\"{_settings.ClientName}\", " + ++ $"Device=\"{_settings.DeviceName}\", " + ++ $"DeviceId=\"{_settings.DeviceId}\", " + ++ $"Version=\"{_settings.ClientVersion}\", " + ++ $"Token=\"{_settings.ApiKey}\""; ++ } ++ ++ /// ++ /// Sends a GET request to the Jellyfin server. ++ /// If endpoint already contains query parameters, they will be preserved and merged with queryParams. ++ /// ++ public async Task GetJsonAsync(string endpoint, Dictionary? queryParams = null, IHeaderDictionary? clientHeaders = null) ++ { ++ // If endpoint contains query string, parse and merge with queryParams ++ if (endpoint.Contains('?')) ++ { ++ var parts = endpoint.Split('?', 2); ++ var baseEndpoint = parts[0]; ++ var existingQuery = parts[1]; ++ ++ // Parse existing query string ++ var mergedParams = new Dictionary(); ++ foreach (var param in existingQuery.Split('&')) ++ { ++ var kv = param.Split('=', 2); ++ if (kv.Length == 2) ++ { ++ mergedParams[Uri.UnescapeDataString(kv[0])] = Uri.UnescapeDataString(kv[1]); ++ } ++ } ++ ++ // Merge with provided queryParams (provided params take precedence) ++ if (queryParams != null) ++ { ++ foreach (var kv in queryParams) ++ { ++ mergedParams[kv.Key] = kv.Value; ++ } ++ } ++ ++ var url = BuildUrl(baseEndpoint, mergedParams); ++ return await GetJsonAsyncInternal(url, clientHeaders); ++ } ++ ++ var finalUrl = BuildUrl(endpoint, queryParams); ++ return await GetJsonAsyncInternal(finalUrl, clientHeaders); ++ } ++ ++ private async Task GetJsonAsyncInternal(string url, IHeaderDictionary? clientHeaders) ++ { ++ using var request = new HttpRequestMessage(HttpMethod.Get, url); ++ ++ // Forward authentication headers from client if provided ++ if (clientHeaders != null) ++ { ++ if (clientHeaders.TryGetValue("X-Emby-Authorization", out var embyAuth)) ++ { ++ request.Headers.TryAddWithoutValidation("X-Emby-Authorization", embyAuth.ToString()); ++ } ++ else if (clientHeaders.TryGetValue("Authorization", out var auth)) ++ { ++ request.Headers.TryAddWithoutValidation("Authorization", auth.ToString()); ++ } ++ } ++ ++ // Only use API key for server-initiated requests (when no client headers provided) ++ // This ensures client requests use the logged-in user's permissions ++ if (clientHeaders == null && !request.Headers.Contains("X-Emby-Authorization") && !request.Headers.Contains("Authorization")) ++ { ++ if (!string.IsNullOrEmpty(_settings.ApiKey)) ++ { ++ request.Headers.Add("Authorization", GetAuthorizationHeader()); ++ } ++ } ++ ++ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); ++ ++ var response = await _httpClient.SendAsync(request); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ _logger.LogWarning("Jellyfin request failed: {StatusCode} for {Url}", response.StatusCode, url); ++ return null; ++ } ++ ++ var content = await response.Content.ReadAsStringAsync(); ++ return JsonDocument.Parse(content); ++ } ++ ++ /// ++ /// Sends a POST request to the Jellyfin server with JSON body. ++ /// Forwards client headers for authentication passthrough. ++ /// ++ public async Task PostJsonAsync(string endpoint, string body, IHeaderDictionary clientHeaders) ++ { ++ var url = BuildUrl(endpoint, null); ++ ++ using var request = new HttpRequestMessage(HttpMethod.Post, url); ++ ++ // Create content from body string ++ if (!string.IsNullOrEmpty(body)) ++ { ++ request.Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json"); ++ _logger.LogDebug("POST body length: {Length} bytes", body.Length); ++ } ++ else ++ { ++ _logger.LogWarning("POST body is empty for {Url}", url); ++ } ++ ++ // For auth endpoints, we need X-Emby-Authorization header with client info (no token yet) ++ // Jellyfin requires this header format even for login ++ if (clientHeaders.TryGetValue("X-Emby-Authorization", out var embyAuth)) ++ { ++ request.Headers.TryAddWithoutValidation("X-Emby-Authorization", embyAuth.ToString()); ++ _logger.LogDebug("Forwarding X-Emby-Authorization: {Header}", embyAuth.ToString()); ++ } ++ else if (clientHeaders.TryGetValue("Authorization", out var auth)) ++ { ++ request.Headers.TryAddWithoutValidation("Authorization", auth.ToString()); ++ _logger.LogDebug("Forwarding Authorization: {Header}", auth.ToString()); ++ } ++ else ++ { ++ // For login requests, provide a minimal client auth header (no token) ++ var clientAuthHeader = $"MediaBrowser Client=\"{_settings.ClientName}\", " + ++ $"Device=\"{_settings.DeviceName}\", " + ++ $"DeviceId=\"{_settings.DeviceId}\", " + ++ $"Version=\"{_settings.ClientVersion}\""; ++ request.Headers.TryAddWithoutValidation("X-Emby-Authorization", clientAuthHeader); ++ _logger.LogDebug("Using default X-Emby-Authorization for login: {Header}", clientAuthHeader); ++ } ++ ++ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); ++ ++ // DO NOT log the body for auth endpoints - it contains passwords! ++ if (endpoint.Contains("Authenticate", StringComparison.OrdinalIgnoreCase)) ++ { ++ _logger.LogDebug("POST to Jellyfin: {Url} (auth request - body not logged)", url); ++ } ++ else ++ { ++ _logger.LogInformation("POST to Jellyfin: {Url}, body length: {Length} bytes", url, body.Length); ++ ++ // Log body content for playback endpoints to debug ++ if (endpoint.Contains("Playing", StringComparison.OrdinalIgnoreCase)) ++ { ++ _logger.LogInformation("Sending body to Jellyfin: {Body}", body); ++ } ++ } ++ ++ var response = await _httpClient.SendAsync(request); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ var errorContent = await response.Content.ReadAsStringAsync(); ++ _logger.LogWarning("Jellyfin POST request failed: {StatusCode} for {Url}. Response: {Response}", ++ response.StatusCode, url, errorContent); ++ return null; ++ } ++ ++ // Handle 204 No Content responses (e.g., /sessions/playing, /sessions/playing/progress) ++ if (response.StatusCode == System.Net.HttpStatusCode.NoContent) ++ { ++ return null; ++ } ++ ++ var responseContent = await response.Content.ReadAsStringAsync(); ++ ++ // Handle empty responses ++ if (string.IsNullOrWhiteSpace(responseContent)) ++ { ++ return null; ++ } ++ ++ return JsonDocument.Parse(responseContent); ++ } ++ ++ /// ++ /// Sends a GET request and returns raw bytes (for images, audio streams). ++ /// ++ public async Task<(byte[] Body, string? ContentType)> GetBytesAsync(string endpoint, Dictionary? queryParams = null) ++ { ++ var url = BuildUrl(endpoint, queryParams); ++ ++ using var request = new HttpRequestMessage(HttpMethod.Get, url); ++ request.Headers.Add("Authorization", GetAuthorizationHeader()); ++ ++ var response = await _httpClient.SendAsync(request); ++ response.EnsureSuccessStatusCode(); ++ ++ var body = await response.Content.ReadAsByteArrayAsync(); ++ var contentType = response.Content.Headers.ContentType?.ToString(); ++ ++ return (body, contentType); ++ } ++ ++ /// ++ /// Safely sends a GET request to the Jellyfin server, returning null on failure. ++ /// ++ public async Task<(byte[]? Body, string? ContentType, bool Success)> GetBytesSafeAsync( ++ string endpoint, ++ Dictionary? queryParams = null) ++ { ++ try ++ { ++ var result = await GetBytesAsync(endpoint, queryParams); ++ return (result.Body, result.ContentType, true); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to get bytes from {Endpoint}", endpoint); ++ return (null, null, false); ++ } ++ } ++ ++ /// ++ /// Searches for items in Jellyfin. ++ /// Uses configured or auto-detected LibraryId to filter search to music library only. ++ /// ++ public async Task SearchAsync( ++ string searchTerm, ++ string[]? includeItemTypes = null, ++ int limit = 20, ++ bool recursive = true, ++ IHeaderDictionary? clientHeaders = null) ++ { ++ var queryParams = new Dictionary ++ { ++ ["searchTerm"] = searchTerm, ++ ["limit"] = limit.ToString(), ++ ["recursive"] = recursive.ToString().ToLower(), ++ ["fields"] = "PrimaryImageAspectRatio,MediaSources,Path,Genres,Studios,DateCreated,Overview,ProviderIds" ++ }; ++ ++ if (!string.IsNullOrEmpty(_settings.UserId)) ++ { ++ queryParams["userId"] = _settings.UserId; ++ } ++ ++ // Only filter search to music library if explicitly configured ++ if (!string.IsNullOrEmpty(_settings.LibraryId)) ++ { ++ queryParams["parentId"] = _settings.LibraryId; ++ _logger.LogDebug("Searching within configured LibraryId {LibraryId}", _settings.LibraryId); ++ } ++ ++ if (includeItemTypes != null && includeItemTypes.Length > 0) ++ { ++ queryParams["includeItemTypes"] = string.Join(",", includeItemTypes); ++ } ++ ++ return await GetJsonAsync("Items", queryParams, clientHeaders); ++ } ++ ++ /// ++ /// Gets items from a specific parent (album, artist, playlist). ++ /// ++ public async Task GetItemsAsync( ++ string? parentId = null, ++ string[]? includeItemTypes = null, ++ string? sortBy = null, ++ int? limit = null, ++ int? startIndex = null, ++ IHeaderDictionary? clientHeaders = null) ++ { ++ var queryParams = new Dictionary ++ { ++ ["recursive"] = "true", ++ ["fields"] = "PrimaryImageAspectRatio,MediaSources,Path,Genres,Studios,DateCreated,Overview,ProviderIds,ParentId" ++ }; ++ ++ if (!string.IsNullOrEmpty(_settings.UserId)) ++ { ++ queryParams["userId"] = _settings.UserId; ++ } ++ ++ if (!string.IsNullOrEmpty(parentId)) ++ { ++ queryParams["parentId"] = parentId; ++ } ++ ++ if (includeItemTypes != null && includeItemTypes.Length > 0) ++ { ++ queryParams["includeItemTypes"] = string.Join(",", includeItemTypes); ++ } ++ ++ if (!string.IsNullOrEmpty(sortBy)) ++ { ++ queryParams["sortBy"] = sortBy; ++ } ++ ++ if (limit.HasValue) ++ { ++ queryParams["limit"] = limit.Value.ToString(); ++ } ++ ++ if (startIndex.HasValue) ++ { ++ queryParams["startIndex"] = startIndex.Value.ToString(); ++ } ++ ++ return await GetJsonAsync("Items", queryParams, clientHeaders); ++ } ++ ++ /// ++ /// Gets a single item by ID. ++ /// ++ public async Task GetItemAsync(string itemId, IHeaderDictionary? clientHeaders = null) ++ { ++ var queryParams = new Dictionary(); ++ ++ if (!string.IsNullOrEmpty(_settings.UserId)) ++ { ++ queryParams["userId"] = _settings.UserId; ++ } ++ ++ return await GetJsonAsync($"Items/{itemId}", queryParams, clientHeaders); ++ } ++ ++ /// ++ /// Gets artists from the library. ++ /// ++ public async Task GetArtistsAsync( ++ string? searchTerm = null, ++ int? limit = null, ++ int? startIndex = null, ++ IHeaderDictionary? clientHeaders = null) ++ { ++ var queryParams = new Dictionary ++ { ++ ["fields"] = "PrimaryImageAspectRatio,Genres,Overview" ++ }; ++ ++ if (!string.IsNullOrEmpty(_settings.UserId)) ++ { ++ queryParams["userId"] = _settings.UserId; ++ } ++ ++ if (!string.IsNullOrEmpty(searchTerm)) ++ { ++ queryParams["searchTerm"] = searchTerm; ++ } ++ ++ if (limit.HasValue) ++ { ++ queryParams["limit"] = limit.Value.ToString(); ++ } ++ ++ if (startIndex.HasValue) ++ { ++ queryParams["startIndex"] = startIndex.Value.ToString(); ++ } ++ ++ return await GetJsonAsync("Artists", queryParams, clientHeaders); ++ } ++ ++ /// ++ /// Gets an artist by name or ID. ++ /// ++ public async Task GetArtistAsync(string artistIdOrName, IHeaderDictionary? clientHeaders = null) ++ { ++ var queryParams = new Dictionary(); ++ ++ if (!string.IsNullOrEmpty(_settings.UserId)) ++ { ++ queryParams["userId"] = _settings.UserId; ++ } ++ ++ // Try to get by ID first ++ if (Guid.TryParse(artistIdOrName, out _)) ++ { ++ return await GetJsonAsync($"Items/{artistIdOrName}", queryParams, clientHeaders); ++ } ++ ++ // Otherwise search by name ++ return await GetJsonAsync($"Artists/{Uri.EscapeDataString(artistIdOrName)}", queryParams, clientHeaders); ++ } ++ ++ /// ++ /// Streams audio from Jellyfin with range support. ++ /// ++ public async Task StreamAudioAsync( ++ string itemId, ++ CancellationToken cancellationToken) ++ { ++ try ++ { ++ var httpContext = _httpContextAccessor.HttpContext; ++ if (httpContext == null) ++ { ++ return new ObjectResult(new { error = "HTTP context not available" }) ++ { ++ StatusCode = 500 ++ }; ++ } ++ ++ var incomingRequest = httpContext.Request; ++ var outgoingResponse = httpContext.Response; ++ ++ // Build the stream URL - use static streaming for simplicity ++ var queryParams = new Dictionary ++ { ++ ["static"] = "true", ++ ["mediaSourceId"] = itemId ++ }; ++ ++ var url = BuildUrl($"Audio/{itemId}/stream", queryParams); ++ ++ using var request = new HttpRequestMessage(HttpMethod.Get, url); ++ request.Headers.Add("Authorization", GetAuthorizationHeader()); ++ ++ // Forward Range headers for progressive streaming ++ if (incomingRequest.Headers.TryGetValue("Range", out var range)) ++ { ++ request.Headers.TryAddWithoutValidation("Range", range.ToArray()); ++ } ++ ++ if (incomingRequest.Headers.TryGetValue("If-Range", out var ifRange)) ++ { ++ request.Headers.TryAddWithoutValidation("If-Range", ifRange.ToArray()); ++ } ++ ++ var response = await _httpClient.SendAsync( ++ request, ++ HttpCompletionOption.ResponseHeadersRead, ++ cancellationToken); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ return new StatusCodeResult((int)response.StatusCode); ++ } ++ ++ // Forward HTTP status code ++ outgoingResponse.StatusCode = (int)response.StatusCode; ++ ++ // Forward streaming headers ++ var streamingHeaders = new[] { "Accept-Ranges", "Content-Range", "Content-Length", "ETag", "Last-Modified" }; ++ foreach (var header in streamingHeaders) ++ { ++ if (response.Headers.TryGetValues(header, out var values) || ++ response.Content.Headers.TryGetValues(header, out values)) ++ { ++ outgoingResponse.Headers[header] = values.ToArray(); ++ } ++ } ++ ++ var stream = await response.Content.ReadAsStreamAsync(cancellationToken); ++ var contentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg"; ++ ++ return new FileStreamResult(stream, contentType) ++ { ++ EnableRangeProcessing = true ++ }; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error streaming from Jellyfin item {ItemId}", itemId); ++ return new ObjectResult(new { error = $"Error streaming: {ex.Message}" }) ++ { ++ StatusCode = 500 ++ }; ++ } ++ } ++ ++ /// ++ /// Gets the image for an item. ++ /// ++ public async Task<(byte[]? Body, string? ContentType)> GetImageAsync( ++ string itemId, ++ string imageType = "Primary", ++ int? maxWidth = null, ++ int? maxHeight = null) ++ { ++ // Build cache key ++ var cacheKey = $"image:{itemId}:{imageType}:{maxWidth}:{maxHeight}"; ++ ++ // Try cache first ++ var cached = await _cache.GetStringAsync(cacheKey); ++ if (!string.IsNullOrEmpty(cached)) ++ { ++ var parts = cached.Split('|', 2); ++ if (parts.Length == 2) ++ { ++ var body = Convert.FromBase64String(parts[0]); ++ var contentType = parts[1]; ++ return (body, contentType); ++ } ++ } ++ ++ var queryParams = new Dictionary(); ++ ++ if (maxWidth.HasValue) ++ { ++ queryParams["maxWidth"] = maxWidth.Value.ToString(); ++ } ++ ++ if (maxHeight.HasValue) ++ { ++ queryParams["maxHeight"] = maxHeight.Value.ToString(); ++ } ++ ++ var result = await GetBytesSafeAsync($"Items/{itemId}/Images/{imageType}", queryParams); ++ ++ // Cache for 7 days if successful ++ if (result.Success && result.Body != null) ++ { ++ var cacheValue = $"{Convert.ToBase64String(result.Body)}|{result.ContentType}"; ++ await _cache.SetStringAsync(cacheKey, cacheValue, TimeSpan.FromDays(7)); ++ } ++ ++ return (result.Body, result.ContentType); ++ } ++ ++ /// ++ /// Tests connection to the Jellyfin server. ++ /// ++ public async Task<(bool Success, string? ServerName, string? Version)> TestConnectionAsync() ++ { ++ try ++ { ++ var result = await GetJsonAsync("System/Info/Public"); ++ if (result == null) ++ { ++ return (false, null, null); ++ } ++ ++ var serverName = result.RootElement.TryGetProperty("ServerName", out var name) ++ ? name.GetString() ++ : null; ++ var version = result.RootElement.TryGetProperty("Version", out var ver) ++ ? ver.GetString() ++ : null; ++ ++ return (true, serverName, version); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to test Jellyfin connection"); ++ return (false, null, null); ++ } ++ } ++ ++ /// ++ /// Gets the music library ID from Jellyfin by querying media folders. ++ /// ++ private async Task GetMusicLibraryIdInternalAsync() ++ { ++ try ++ { ++ var queryParams = new Dictionary(); ++ if (!string.IsNullOrEmpty(_settings.UserId)) ++ { ++ queryParams["userId"] = _settings.UserId; ++ } ++ ++ var result = await GetJsonAsync("Library/MediaFolders", queryParams); ++ if (result == null) ++ { ++ return null; ++ } ++ ++ if (result.RootElement.TryGetProperty("Items", out var items)) ++ { ++ foreach (var item in items.EnumerateArray()) ++ { ++ var collectionType = item.TryGetProperty("CollectionType", out var ct) ++ ? ct.GetString() ++ : null; ++ ++ if (collectionType == "music") ++ { ++ return item.TryGetProperty("Id", out var id) ++ ? id.GetString() ++ : null; ++ } ++ } ++ } ++ ++ return null; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to get music library ID"); ++ return null; ++ } ++ } ++ ++ private string BuildUrl(string endpoint, Dictionary? queryParams = null) ++ { ++ var baseUrl = _settings.Url?.TrimEnd('/') ?? ""; ++ var url = $"{baseUrl}/{endpoint}"; ++ ++ if (queryParams != null && queryParams.Count > 0) ++ { ++ var query = string.Join("&", queryParams.Select(kv => ++ $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); ++ url = $"{url}?{query}"; ++ } ++ ++ return url; ++ } ++} +diff --git a/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs +new file mode 100644 +index 0000000..7d67a06 +--- /dev/null ++++ b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs +@@ -0,0 +1,513 @@ ++using Microsoft.AspNetCore.Mvc; ++using System.Text.Json; ++using allstarr.Models.Domain; ++using allstarr.Models.Subsonic; ++ ++namespace allstarr.Services.Jellyfin; ++ ++/// ++/// Builds Jellyfin-compatible API responses. ++/// ++public class JellyfinResponseBuilder ++{ ++ /// ++ /// Creates a Jellyfin items response containing songs. ++ /// ++ public IActionResult CreateItemsResponse(List songs) ++ { ++ var items = songs.Select(ConvertSongToJellyfinItem).ToList(); ++ ++ return CreateJsonResponse(new ++ { ++ Items = items, ++ TotalRecordCount = items.Count, ++ StartIndex = 0 ++ }); ++ } ++ ++ /// ++ /// Creates a Jellyfin items response for albums. ++ /// ++ public IActionResult CreateAlbumsResponse(List albums) ++ { ++ var items = albums.Select(ConvertAlbumToJellyfinItem).ToList(); ++ ++ return CreateJsonResponse(new ++ { ++ Items = items, ++ TotalRecordCount = items.Count, ++ StartIndex = 0 ++ }); ++ } ++ ++ /// ++ /// Creates a Jellyfin items response for artists. ++ /// ++ public IActionResult CreateArtistsResponse(List artists) ++ { ++ var items = artists.Select(ConvertArtistToJellyfinItem).ToList(); ++ ++ return CreateJsonResponse(new ++ { ++ Items = items, ++ TotalRecordCount = items.Count, ++ StartIndex = 0 ++ }); ++ } ++ ++ /// ++ /// Creates a single item response. ++ /// ++ public IActionResult CreateSongResponse(Song song) ++ { ++ return CreateJsonResponse(ConvertSongToJellyfinItem(song)); ++ } ++ ++ /// ++ /// Creates a single album response with tracks. ++ /// ++ public IActionResult CreateAlbumResponse(Album album) ++ { ++ var albumItem = ConvertAlbumToJellyfinItem(album); ++ ++ // For album detail, include child items (songs) ++ if (album.Songs.Count > 0) ++ { ++ albumItem["Children"] = album.Songs.Select(ConvertSongToJellyfinItem).ToList(); ++ } ++ ++ return CreateJsonResponse(albumItem); ++ } ++ ++ /// ++ /// Creates a single artist response with albums. ++ /// ++ public IActionResult CreateArtistResponse(Artist artist, List albums) ++ { ++ var artistItem = ConvertArtistToJellyfinItem(artist); ++ artistItem["Albums"] = albums.Select(ConvertAlbumToJellyfinItem).ToList(); ++ ++ return CreateJsonResponse(artistItem); ++ } ++ ++ /// ++ /// Creates a response for a playlist represented as an album. ++ /// ++ public IActionResult CreatePlaylistAsAlbumResponse(ExternalPlaylist playlist, List tracks) ++ { ++ var totalDuration = tracks.Sum(s => s.Duration ?? 0); ++ ++ var curatorName = !string.IsNullOrEmpty(playlist.CuratorName) ++ ? playlist.CuratorName ++ : playlist.Provider; ++ ++ var albumItem = new Dictionary ++ { ++ ["Id"] = playlist.Id, ++ ["Name"] = playlist.Name, ++ ["Type"] = "Playlist", ++ ["AlbumArtist"] = curatorName, ++ ["Genres"] = new[] { "Playlist" }, ++ ["ChildCount"] = tracks.Count, ++ ["RunTimeTicks"] = totalDuration * TimeSpan.TicksPerSecond, ++ ["ImageTags"] = new Dictionary ++ { ++ ["Primary"] = playlist.Id ++ }, ++ ["ProviderIds"] = new Dictionary ++ { ++ [playlist.Provider] = playlist.ExternalId ++ }, ++ ["Children"] = tracks.Select(ConvertSongToJellyfinItem).ToList() ++ }; ++ ++ if (playlist.CreatedDate.HasValue) ++ { ++ albumItem["PremiereDate"] = playlist.CreatedDate.Value.ToString("o"); ++ albumItem["ProductionYear"] = playlist.CreatedDate.Value.Year; ++ } ++ ++ return CreateJsonResponse(albumItem); ++ } ++ ++ /// ++ /// Creates a search hints response (Jellyfin search format). ++ /// ++ public IActionResult CreateSearchHintsResponse( ++ List songs, ++ List albums, ++ List artists) ++ { ++ var searchHints = new List>(); ++ ++ // Add artists first ++ foreach (var artist in artists) ++ { ++ searchHints.Add(new Dictionary ++ { ++ ["Id"] = artist.Id, ++ ["Name"] = artist.Name, ++ ["Type"] = "MusicArtist", ++ ["RunTimeTicks"] = 0, ++ ["PrimaryImageAspectRatio"] = 1.0, ++ ["ImageTags"] = new Dictionary ++ { ++ ["Primary"] = artist.Id ++ } ++ }); ++ } ++ ++ // Add albums ++ foreach (var album in albums) ++ { ++ searchHints.Add(new Dictionary ++ { ++ ["Id"] = album.Id, ++ ["Name"] = album.Title, ++ ["Type"] = "MusicAlbum", ++ ["Album"] = album.Title, ++ ["AlbumArtist"] = album.Artist, ++ ["ProductionYear"] = album.Year, ++ ["RunTimeTicks"] = 0, ++ ["ImageTags"] = new Dictionary ++ { ++ ["Primary"] = album.Id ++ } ++ }); ++ } ++ ++ // Add songs ++ foreach (var song in songs) ++ { ++ searchHints.Add(new Dictionary ++ { ++ ["Id"] = song.Id, ++ ["Name"] = song.Title, ++ ["Type"] = "Audio", ++ ["Album"] = song.Album, ++ ["AlbumArtist"] = song.Artist, ++ ["Artists"] = new[] { song.Artist }, ++ ["RunTimeTicks"] = (song.Duration ?? 0) * TimeSpan.TicksPerSecond, ++ ["ImageTags"] = new Dictionary ++ { ++ ["Primary"] = song.Id ++ } ++ }); ++ } ++ ++ return CreateJsonResponse(new ++ { ++ SearchHints = searchHints, ++ TotalRecordCount = searchHints.Count ++ }); ++ } ++ ++ /// ++ /// Creates an error response in Jellyfin format. ++ /// ++ public IActionResult CreateError(int statusCode, string message) ++ { ++ return new ObjectResult(new ++ { ++ type = "https://tools.ietf.org/html/rfc7231#section-6.5.4", ++ title = message, ++ status = statusCode ++ }) ++ { ++ StatusCode = statusCode ++ }; ++ } ++ ++ /// ++ /// Creates a JSON response. ++ /// ++ public IActionResult CreateJsonResponse(object data) ++ { ++ return new JsonResult(data); ++ } ++ ++ /// ++ /// Converts a Song domain model to a Jellyfin item. ++ /// ++ public Dictionary ConvertSongToJellyfinItem(Song song) ++ { ++ var item = new Dictionary ++ { ++ ["Id"] = song.Id, ++ ["Name"] = song.Title, ++ ["ServerId"] = "allstarr", ++ ["Type"] = "Audio", ++ ["MediaType"] = "Audio", ++ ["IsFolder"] = false, ++ ["Album"] = song.Album, ++ ["AlbumId"] = song.AlbumId ?? song.Id, ++ ["AlbumArtist"] = song.AlbumArtist ?? song.Artist, ++ ["Artists"] = new[] { song.Artist }, ++ ["ArtistItems"] = new[] ++ { ++ new Dictionary ++ { ++ ["Id"] = song.ArtistId ?? song.Id, ++ ["Name"] = song.Artist ++ } ++ }, ++ ["IndexNumber"] = song.Track, ++ ["ParentIndexNumber"] = song.DiscNumber ?? 1, ++ ["ProductionYear"] = song.Year, ++ ["RunTimeTicks"] = (song.Duration ?? 0) * TimeSpan.TicksPerSecond, ++ ["ImageTags"] = new Dictionary ++ { ++ ["Primary"] = song.Id ++ }, ++ ["BackdropImageTags"] = new string[0], ++ ["ImageBlurHashes"] = new Dictionary(), ++ ["LocationType"] = "FileSystem", // External content appears as local files to clients ++ ["Path"] = $"/music/{song.Artist}/{song.Album}/{song.Title}.flac", // Fake path for client compatibility ++ ["ChannelId"] = (object?)null, // Match Jellyfin structure ++ ["UserData"] = new Dictionary ++ { ++ ["PlaybackPositionTicks"] = 0, ++ ["PlayCount"] = 0, ++ ["IsFavorite"] = false, ++ ["Played"] = false, ++ ["Key"] = $"Audio-{song.Id}" ++ }, ++ ["CanDownload"] = true, ++ ["SupportsSync"] = true ++ }; ++ ++ // Add provider IDs for external content ++ if (!song.IsLocal && !string.IsNullOrEmpty(song.ExternalProvider)) ++ { ++ item["ProviderIds"] = new Dictionary ++ { ++ [song.ExternalProvider] = song.ExternalId ?? "" ++ }; ++ ++ if (!string.IsNullOrEmpty(song.Isrc)) ++ { ++ var providerIds = (Dictionary)item["ProviderIds"]!; ++ providerIds["ISRC"] = song.Isrc; ++ } ++ } ++ ++ if (!string.IsNullOrEmpty(song.Genre)) ++ { ++ item["Genres"] = new[] { song.Genre }; ++ } ++ ++ return item; ++ } ++ ++ /// ++ /// Converts an Album domain model to a Jellyfin item. ++ /// ++ public Dictionary ConvertAlbumToJellyfinItem(Album album) ++ { ++ var item = new Dictionary ++ { ++ ["Id"] = album.Id, ++ ["Name"] = album.Title, ++ ["ServerId"] = "allstarr", ++ ["Type"] = "MusicAlbum", ++ ["IsFolder"] = true, ++ ["AlbumArtist"] = album.Artist, ++ ["AlbumArtists"] = new[] ++ { ++ new Dictionary ++ { ++ ["Id"] = album.ArtistId ?? album.Id, ++ ["Name"] = album.Artist ++ } ++ }, ++ ["ProductionYear"] = album.Year, ++ ["ChildCount"] = album.SongCount ?? album.Songs.Count, ++ ["ImageTags"] = new Dictionary ++ { ++ ["Primary"] = album.Id ++ }, ++ ["BackdropImageTags"] = new string[0], ++ ["ImageBlurHashes"] = new Dictionary(), ++ ["LocationType"] = "FileSystem", // External content appears as local files to clients ++ ["MediaType"] = (object?)null, // Match Jellyfin structure ++ ["ChannelId"] = (object?)null, // Match Jellyfin structure ++ ["CollectionType"] = (object?)null, // Match Jellyfin structure ++ ["UserData"] = new Dictionary ++ { ++ ["PlaybackPositionTicks"] = 0, ++ ["PlayCount"] = 0, ++ ["IsFavorite"] = false, ++ ["Played"] = false, ++ ["Key"] = album.Id ++ } ++ }; ++ ++ // Add provider IDs for external content ++ if (!album.IsLocal && !string.IsNullOrEmpty(album.ExternalProvider)) ++ { ++ item["ProviderIds"] = new Dictionary ++ { ++ [album.ExternalProvider] = album.ExternalId ?? "" ++ }; ++ } ++ ++ if (!string.IsNullOrEmpty(album.Genre)) ++ { ++ item["Genres"] = new[] { album.Genre }; ++ } ++ ++ return item; ++ } ++ ++ /// ++ /// Converts an Artist domain model to a Jellyfin item. ++ /// ++ public Dictionary ConvertArtistToJellyfinItem(Artist artist) ++ { ++ var item = new Dictionary ++ { ++ ["Id"] = artist.Id, ++ ["Name"] = artist.Name, ++ ["ServerId"] = "allstarr", ++ ["Type"] = "MusicArtist", ++ ["IsFolder"] = true, ++ ["AlbumCount"] = artist.AlbumCount ?? 0, ++ ["ImageTags"] = new Dictionary ++ { ++ ["Primary"] = artist.Id ++ }, ++ ["BackdropImageTags"] = new string[0], ++ ["ImageBlurHashes"] = new Dictionary(), ++ ["LocationType"] = "FileSystem", // External content appears as local files to clients ++ ["MediaType"] = (object?)null, // Match Jellyfin structure ++ ["ChannelId"] = (object?)null, // Match Jellyfin structure ++ ["CollectionType"] = (object?)null, // Match Jellyfin structure ++ ["UserData"] = new Dictionary ++ { ++ ["PlaybackPositionTicks"] = 0, ++ ["PlayCount"] = 0, ++ ["IsFavorite"] = false, ++ ["Played"] = false, ++ ["Key"] = artist.Id ++ } ++ }; ++ ++ // Add provider IDs for external content ++ if (!artist.IsLocal && !string.IsNullOrEmpty(artist.ExternalProvider)) ++ { ++ item["ProviderIds"] = new Dictionary ++ { ++ [artist.ExternalProvider] = artist.ExternalId ?? "" ++ }; ++ } ++ ++ return item; ++ } ++ ++ /// ++ /// Converts a Jellyfin JSON element to a dictionary. ++ /// ++ public object ConvertJellyfinJsonElement(JsonElement element) ++ { ++ return element.ValueKind switch ++ { ++ JsonValueKind.Object => element.EnumerateObject() ++ .ToDictionary(p => p.Name, p => ConvertJellyfinJsonElement(p.Value)), ++ JsonValueKind.Array => element.EnumerateArray() ++ .Select(ConvertJellyfinJsonElement) ++ .ToList(), ++ JsonValueKind.String => element.GetString() ?? "", ++ JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(), ++ JsonValueKind.True => true, ++ JsonValueKind.False => false, ++ JsonValueKind.Null => null!, ++ _ => element.ToString() ++ }; ++ } ++ ++ /// ++ /// Converts an ExternalPlaylist to a Jellyfin album item. ++ /// ++ public Dictionary ConvertPlaylistToJellyfinItem(ExternalPlaylist playlist) ++ { ++ var curatorName = !string.IsNullOrEmpty(playlist.CuratorName) ++ ? playlist.CuratorName ++ : playlist.Provider; ++ ++ var item = new Dictionary ++ { ++ ["Id"] = playlist.Id, ++ ["Name"] = playlist.Name, ++ ["ServerId"] = "allstarr", ++ ["Type"] = "Playlist", ++ ["IsFolder"] = true, ++ ["AlbumArtist"] = curatorName, ++ ["Genres"] = new[] { "Playlist" }, ++ ["ChildCount"] = playlist.TrackCount, ++ ["ImageTags"] = new Dictionary ++ { ++ ["Primary"] = playlist.Id ++ }, ++ ["BackdropImageTags"] = new string[0], ++ ["ImageBlurHashes"] = new Dictionary(), ++ ["LocationType"] = "FileSystem", ++ ["MediaType"] = (object?)null, ++ ["ChannelId"] = (object?)null, ++ ["CollectionType"] = (object?)null, ++ ["ProviderIds"] = new Dictionary ++ { ++ [playlist.Provider] = playlist.ExternalId ++ }, ++ ["UserData"] = new Dictionary ++ { ++ ["PlaybackPositionTicks"] = 0, ++ ["PlayCount"] = 0, ++ ["IsFavorite"] = false, ++ ["Played"] = false, ++ ["Key"] = playlist.Id ++ } ++ }; ++ ++ if (playlist.CreatedDate.HasValue) ++ { ++ item["PremiereDate"] = playlist.CreatedDate.Value.ToString("o"); ++ item["ProductionYear"] = playlist.CreatedDate.Value.Year; ++ } ++ ++ return item; ++ } ++ public Dictionary ConvertPlaylistToAlbumItem(ExternalPlaylist playlist) ++ { ++ var curatorName = !string.IsNullOrEmpty(playlist.CuratorName) ++ ? playlist.CuratorName ++ : playlist.Provider; ++ ++ var item = new Dictionary ++ { ++ ["Id"] = playlist.Id, ++ ["Name"] = playlist.Name, ++ ["Type"] = "Playlist", ++ ["IsFolder"] = true, ++ ["AlbumArtist"] = curatorName, ++ ["ChildCount"] = playlist.TrackCount, ++ ["RunTimeTicks"] = playlist.Duration * TimeSpan.TicksPerSecond, ++ ["Genres"] = new[] { "Playlist" }, ++ ["ImageTags"] = new Dictionary ++ { ++ ["Primary"] = playlist.Id ++ }, ++ ["ProviderIds"] = new Dictionary ++ { ++ [playlist.Provider] = playlist.ExternalId ++ } ++ }; ++ ++ if (playlist.CreatedDate.HasValue) ++ { ++ item["PremiereDate"] = playlist.CreatedDate.Value.ToString("o"); ++ item["ProductionYear"] = playlist.CreatedDate.Value.Year; ++ } ++ ++ return item; ++ } ++} +diff --git a/allstarr/Services/Jellyfin/JellyfinStartupValidator.cs b/allstarr/Services/Jellyfin/JellyfinStartupValidator.cs +new file mode 100644 +index 0000000..c4977e8 +--- /dev/null ++++ b/allstarr/Services/Jellyfin/JellyfinStartupValidator.cs +@@ -0,0 +1,214 @@ ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++using allstarr.Services.Validation; ++ ++namespace allstarr.Services.Jellyfin; ++ ++/// ++/// Validates Jellyfin server connectivity at startup. ++/// ++public class JellyfinStartupValidator : BaseStartupValidator ++{ ++ private readonly IOptions _settings; ++ ++ public override string ServiceName => "Jellyfin"; ++ ++ public JellyfinStartupValidator(IOptions settings, HttpClient httpClient) ++ : base(httpClient) ++ { ++ _settings = settings; ++ } ++ ++ public override async Task ValidateAsync(CancellationToken cancellationToken) ++ { ++ var settings = _settings.Value; ++ ++ if (string.IsNullOrWhiteSpace(settings.Url)) ++ { ++ WriteStatus("Jellyfin URL", "NOT CONFIGURED", ConsoleColor.Red); ++ WriteDetail("Set the Jellyfin__Url environment variable"); ++ return ValidationResult.NotConfigured("Jellyfin URL not configured"); ++ } ++ ++ WriteStatus("Jellyfin URL", settings.Url, ConsoleColor.Cyan); ++ ++ // API Key is optional - only needed for server-to-server operations ++ // Client authentication uses username/password via /Users/AuthenticateByName ++ if (!string.IsNullOrWhiteSpace(settings.ApiKey)) ++ { ++ WriteStatus("API Key", MaskSecret(settings.ApiKey), ConsoleColor.DarkGray); ++ WriteDetail("(Optional - for server operations)"); ++ } ++ ++ if (!string.IsNullOrWhiteSpace(settings.UserId)) ++ { ++ WriteStatus("User ID", MaskSecret(settings.UserId), ConsoleColor.DarkGray); ++ WriteDetail("(Optional - for server operations)"); ++ } ++ ++ try ++ { ++ // Test connection using public system info endpoint (no auth required) ++ var publicInfoUrl = $"{settings.Url.TrimEnd('/')}/System/Info/Public"; ++ var response = await _httpClient.GetAsync(publicInfoUrl, cancellationToken); ++ ++ if (response.IsSuccessStatusCode) ++ { ++ var content = await response.Content.ReadAsStringAsync(cancellationToken); ++ ++ // Try to parse server info ++ string? serverName = null; ++ string? version = null; ++ ++ if (content.Contains("ServerName")) ++ { ++ var nameStart = content.IndexOf("\"ServerName\":", StringComparison.Ordinal); ++ if (nameStart >= 0) ++ { ++ nameStart = content.IndexOf('"', nameStart + 13) + 1; ++ var nameEnd = content.IndexOf('"', nameStart); ++ if (nameEnd > nameStart) ++ { ++ serverName = content[nameStart..nameEnd]; ++ } ++ } ++ } ++ ++ if (content.Contains("Version")) ++ { ++ var verStart = content.IndexOf("\"Version\":", StringComparison.Ordinal); ++ if (verStart >= 0) ++ { ++ verStart = content.IndexOf('"', verStart + 10) + 1; ++ var verEnd = content.IndexOf('"', verStart); ++ if (verEnd > verStart) ++ { ++ version = content[verStart..verEnd]; ++ } ++ } ++ } ++ ++ var serverInfo = !string.IsNullOrEmpty(serverName) ++ ? $"{serverName} (v{version ?? "unknown"})" ++ : "OK"; ++ ++ WriteStatus("Jellyfin server", serverInfo, ConsoleColor.Green); ++ ++ // Test authenticated access if API key is configured ++ if (!string.IsNullOrWhiteSpace(settings.ApiKey)) ++ { ++ await ValidateAuthenticatedAccessAsync(settings, cancellationToken); ++ } ++ ++ return ValidationResult.Success($"Connected to {serverInfo}"); ++ } ++ else ++ { ++ WriteStatus("Jellyfin server", $"HTTP {(int)response.StatusCode}", ConsoleColor.Red); ++ return ValidationResult.Failure($"HTTP {(int)response.StatusCode}", ++ "Jellyfin server returned an error", ConsoleColor.Red); ++ } ++ } ++ catch (TaskCanceledException) ++ { ++ WriteStatus("Jellyfin server", "TIMEOUT", ConsoleColor.Red); ++ WriteDetail("Could not reach server within 10 seconds"); ++ return ValidationResult.Failure("TIMEOUT", "Could not reach server within timeout period", ConsoleColor.Red); ++ } ++ catch (HttpRequestException ex) ++ { ++ WriteStatus("Jellyfin server", "UNREACHABLE", ConsoleColor.Red); ++ WriteDetail(ex.Message); ++ return ValidationResult.Failure("UNREACHABLE", ex.Message, ConsoleColor.Red); ++ } ++ catch (Exception ex) ++ { ++ WriteStatus("Jellyfin server", "ERROR", ConsoleColor.Red); ++ WriteDetail(ex.Message); ++ return ValidationResult.Failure("ERROR", ex.Message, ConsoleColor.Red); ++ } ++ } ++ ++ private async Task ValidateAuthenticatedAccessAsync(JellyfinSettings settings, CancellationToken cancellationToken) ++ { ++ try ++ { ++ var authHeader = $"MediaBrowser Client=\"{settings.ClientName}\", " + ++ $"Device=\"{settings.DeviceName}\", " + ++ $"DeviceId=\"{settings.DeviceId}\", " + ++ $"Version=\"{settings.ClientVersion}\", " + ++ $"Token=\"{settings.ApiKey}\""; ++ ++ using var request = new HttpRequestMessage(HttpMethod.Get, ++ $"{settings.Url.TrimEnd('/')}/System/Info"); ++ request.Headers.Add("Authorization", authHeader); ++ ++ var response = await _httpClient.SendAsync(request, cancellationToken); ++ ++ if (response.IsSuccessStatusCode) ++ { ++ WriteStatus("Authentication", "OK", ConsoleColor.Green); ++ } ++ else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) ++ { ++ WriteStatus("Authentication", "INVALID API KEY", ConsoleColor.Red); ++ WriteDetail("Check your Jellyfin API key configuration"); ++ } ++ else ++ { ++ WriteStatus("Authentication", $"HTTP {(int)response.StatusCode}", ConsoleColor.Yellow); ++ } ++ ++ // Check if we can access the music library ++ if (!string.IsNullOrWhiteSpace(settings.LibraryId)) ++ { ++ WriteStatus("Library ID", settings.LibraryId, ConsoleColor.DarkGray); ++ } ++ else ++ { ++ await TryDetectMusicLibraryAsync(settings, authHeader, cancellationToken); ++ } ++ } ++ catch (Exception ex) ++ { ++ WriteStatus("Authentication", "ERROR", ConsoleColor.Yellow); ++ WriteDetail(ex.Message); ++ } ++ } ++ ++ private async Task TryDetectMusicLibraryAsync(JellyfinSettings settings, string authHeader, CancellationToken cancellationToken) ++ { ++ try ++ { ++ var url = $"{settings.Url.TrimEnd('/')}/Library/MediaFolders"; ++ if (!string.IsNullOrWhiteSpace(settings.UserId)) ++ { ++ url += $"?userId={settings.UserId}"; ++ } ++ ++ using var request = new HttpRequestMessage(HttpMethod.Get, url); ++ request.Headers.Add("Authorization", authHeader); ++ ++ var response = await _httpClient.SendAsync(request, cancellationToken); ++ ++ if (response.IsSuccessStatusCode) ++ { ++ var content = await response.Content.ReadAsStringAsync(cancellationToken); ++ ++ if (content.Contains("\"CollectionType\":\"music\"")) ++ { ++ WriteStatus("Music library", "DETECTED", ConsoleColor.Green); ++ } ++ else ++ { ++ WriteStatus("Music library", "NOT FOUND", ConsoleColor.Yellow); ++ WriteDetail("No music library detected. Set Jellyfin__LibraryId to specify one."); ++ } ++ } ++ } ++ catch ++ { ++ // Silently ignore - not critical for startup ++ } ++ } ++} +diff --git a/allstarr/Services/Local/ILocalLibraryService.cs b/allstarr/Services/Local/ILocalLibraryService.cs +new file mode 100644 +index 0000000..2f4818f +--- /dev/null ++++ b/allstarr/Services/Local/ILocalLibraryService.cs +@@ -0,0 +1,50 @@ ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++ ++namespace allstarr.Services.Local; ++ ++/// ++/// Interface for local music library management ++/// ++public interface ILocalLibraryService ++{ ++ /// ++ /// Checks if an external song already exists locally ++ /// ++ Task GetLocalPathForExternalSongAsync(string externalProvider, string externalId); ++ ++ /// ++ /// Registers a downloaded song in the local library ++ /// ++ Task RegisterDownloadedSongAsync(Song song, string localPath); ++ ++ /// ++ /// Gets the mapping between external ID and local ID ++ /// ++ Task GetLocalIdForExternalSongAsync(string externalProvider, string externalId); ++ ++ /// ++ /// Parses a song ID to determine if it is external or local ++ /// ++ (bool isExternal, string? provider, string? externalId) ParseSongId(string songId); ++ ++ /// ++ /// Parses an external ID to extract the provider, type and ID ++ /// Format: ext-{provider}-{type}-{id} (e.g., ext-deezer-artist-259, ext-deezer-album-96126, ext-deezer-song-12345) ++ /// Also supports legacy format: ext-{provider}-{id} (assumes song type) ++ /// ++ (bool isExternal, string? provider, string? type, string? externalId) ParseExternalId(string id); ++ ++ /// ++ /// Triggers a Subsonic library scan ++ /// ++ Task TriggerLibraryScanAsync(); ++ ++ /// ++ /// Gets the current scan status ++ /// ++ Task GetScanStatusAsync(); ++} +diff --git a/allstarr/Services/Local/LocalLibraryService.cs b/allstarr/Services/Local/LocalLibraryService.cs +new file mode 100644 +index 0000000..527a368 +--- /dev/null ++++ b/allstarr/Services/Local/LocalLibraryService.cs +@@ -0,0 +1,275 @@ ++using System.Text.Json; ++using Microsoft.Extensions.Options; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using allstarr.Services; ++ ++namespace allstarr.Services.Local; ++ ++/// ++/// Local library service implementation ++/// Uses a simple JSON file to store mappings (can be replaced with a database) ++/// ++public class LocalLibraryService : ILocalLibraryService ++{ ++ private readonly string _mappingFilePath; ++ private readonly string _downloadDirectory; ++ private readonly HttpClient _httpClient; ++ private readonly SubsonicSettings _subsonicSettings; ++ private readonly ILogger _logger; ++ private Dictionary? _mappings; ++ private readonly SemaphoreSlim _lock = new(1, 1); ++ ++ // Debounce to avoid triggering too many scans ++ private DateTime _lastScanTrigger = DateTime.MinValue; ++ private readonly TimeSpan _scanDebounceInterval = TimeSpan.FromSeconds(30); ++ ++ public LocalLibraryService( ++ IConfiguration configuration, ++ IHttpClientFactory httpClientFactory, ++ IOptions subsonicSettings, ++ ILogger logger) ++ { ++ _downloadDirectory = configuration["Library:DownloadPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "downloads"); ++ _mappingFilePath = Path.Combine(_downloadDirectory, ".mappings.json"); ++ _httpClient = httpClientFactory.CreateClient(); ++ _subsonicSettings = subsonicSettings.Value; ++ _logger = logger; ++ ++ if (!Directory.Exists(_downloadDirectory)) ++ { ++ Directory.CreateDirectory(_downloadDirectory); ++ } ++ } ++ ++ public async Task GetLocalPathForExternalSongAsync(string externalProvider, string externalId) ++ { ++ var mappings = await LoadMappingsAsync(); ++ var key = $"{externalProvider}:{externalId}"; ++ ++ if (mappings.TryGetValue(key, out var mapping) && File.Exists(mapping.LocalPath)) ++ { ++ return mapping.LocalPath; ++ } ++ ++ return null; ++ } ++ ++ public async Task RegisterDownloadedSongAsync(Song song, string localPath) ++ { ++ if (song.ExternalProvider == null || song.ExternalId == null) return; ++ ++ // Load mappings first (this acquires the lock internally if needed) ++ var mappings = await LoadMappingsAsync(); ++ ++ await _lock.WaitAsync(); ++ try ++ { ++ var key = $"{song.ExternalProvider}:{song.ExternalId}"; ++ ++ mappings[key] = new LocalSongMapping ++ { ++ ExternalProvider = song.ExternalProvider, ++ ExternalId = song.ExternalId, ++ LocalPath = localPath, ++ Title = song.Title, ++ Artist = song.Artist, ++ Album = song.Album, ++ DownloadedAt = DateTime.UtcNow ++ }; ++ ++ await SaveMappingsAsync(mappings); ++ } ++ finally ++ { ++ _lock.Release(); ++ } ++ } ++ ++ public async Task GetLocalIdForExternalSongAsync(string externalProvider, string externalId) ++ { ++ // For now, return null as we don't yet have integration ++ // with the Subsonic server to retrieve local ID after scan ++ await Task.CompletedTask; ++ return null; ++ } ++ ++ public (bool isExternal, string? provider, string? externalId) ParseSongId(string songId) ++ { ++ var (isExternal, provider, _, externalId) = ParseExternalId(songId); ++ return (isExternal, provider, externalId); ++ } ++ ++ public (bool isExternal, string? provider, string? type, string? externalId) ParseExternalId(string id) ++ { ++ if (!id.StartsWith("ext-")) ++ { ++ return (false, null, null, null); ++ } ++ ++ var parts = id.Split('-'); ++ ++ // Known types for the new format ++ var knownTypes = new HashSet { "song", "album", "artist" }; ++ ++ // New format: ext-{provider}-{type}-{id} (e.g., ext-deezer-artist-259) ++ // Only use new format if parts[2] is a known type ++ if (parts.Length >= 4 && knownTypes.Contains(parts[2])) ++ { ++ var provider = parts[1]; ++ var type = parts[2]; ++ var externalId = string.Join("-", parts.Skip(3)); // Handle IDs with dashes ++ return (true, provider, type, externalId); ++ } ++ ++ // Legacy format: ext-{provider}-{id} (assumes "song" type for backward compatibility) ++ // This handles both 3-part IDs and 4+ part IDs where parts[2] is NOT a known type ++ if (parts.Length >= 3) ++ { ++ var provider = parts[1]; ++ var externalId = string.Join("-", parts.Skip(2)); // Everything after provider is the ID ++ return (true, provider, "song", externalId); ++ } ++ ++ return (false, null, null, null); ++ } ++ ++ private async Task> LoadMappingsAsync() ++ { ++ // Fast path: return cached mappings if available ++ if (_mappings != null) return _mappings; ++ ++ // Slow path: acquire lock to load from file (prevents race condition) ++ await _lock.WaitAsync(); ++ try ++ { ++ // Double-check after acquiring lock ++ if (_mappings != null) return _mappings; ++ ++ if (File.Exists(_mappingFilePath)) ++ { ++ var json = await File.ReadAllTextAsync(_mappingFilePath); ++ _mappings = System.Text.Json.JsonSerializer.Deserialize>(json) ++ ?? new Dictionary(); ++ } ++ else ++ { ++ _mappings = new Dictionary(); ++ } ++ ++ return _mappings; ++ } ++ finally ++ { ++ _lock.Release(); ++ } ++ } ++ ++ private async Task SaveMappingsAsync(Dictionary mappings) ++ { ++ _mappings = mappings; ++ var json = System.Text.Json.JsonSerializer.Serialize(mappings, new System.Text.Json.JsonSerializerOptions ++ { ++ WriteIndented = true ++ }); ++ await File.WriteAllTextAsync(_mappingFilePath, json); ++ } ++ ++ public string GetDownloadDirectory() => _downloadDirectory; ++ ++ public async Task TriggerLibraryScanAsync() ++ { ++ // Debounce: avoid triggering too many successive scans ++ var now = DateTime.UtcNow; ++ if (now - _lastScanTrigger < _scanDebounceInterval) ++ { ++ _logger.LogDebug("Scan debounced - last scan was {Elapsed}s ago", ++ (now - _lastScanTrigger).TotalSeconds); ++ return true; ++ } ++ ++ _lastScanTrigger = now; ++ ++ try ++ { ++ // Call Subsonic API to trigger a scan ++ // Note: This endpoint works without authentication on most Subsonic/Navidrome servers ++ // when called from localhost. For remote servers requiring auth, this would need ++ // to be refactored to accept credentials from the controller layer. ++ var url = $"{_subsonicSettings.Url}/rest/startScan?f=json"; ++ ++ _logger.LogInformation("Triggering Subsonic library scan..."); ++ ++ var response = await _httpClient.GetAsync(url); ++ ++ if (response.IsSuccessStatusCode) ++ { ++ var content = await response.Content.ReadAsStringAsync(); ++ _logger.LogInformation("Subsonic scan triggered successfully: {Response}", content); ++ return true; ++ } ++ else ++ { ++ _logger.LogWarning("Failed to trigger Subsonic scan: {StatusCode} - Server may require authentication", response.StatusCode); ++ return false; ++ } ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error triggering Subsonic library scan"); ++ return false; ++ } ++ } ++ ++ public async Task GetScanStatusAsync() ++ { ++ try ++ { ++ // Note: This endpoint works without authentication on most Subsonic/Navidrome servers ++ // when called from localhost. ++ var url = $"{_subsonicSettings.Url}/rest/getScanStatus?f=json"; ++ ++ var response = await _httpClient.GetAsync(url); ++ ++ if (response.IsSuccessStatusCode) ++ { ++ var content = await response.Content.ReadAsStringAsync(); ++ var doc = JsonDocument.Parse(content); ++ ++ if (doc.RootElement.TryGetProperty("subsonic-response", out var subsonicResponse) && ++ subsonicResponse.TryGetProperty("scanStatus", out var scanStatus)) ++ { ++ return new ScanStatus ++ { ++ Scanning = scanStatus.TryGetProperty("scanning", out var scanning) && scanning.GetBoolean(), ++ Count = scanStatus.TryGetProperty("count", out var count) ? count.GetInt32() : null ++ }; ++ } ++ } ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Error getting Subsonic scan status"); ++ } ++ ++ return null; ++ } ++} ++ ++/// ++/// Represents the mapping between an external song and its local file ++/// ++public class LocalSongMapping ++{ ++ public string ExternalProvider { get; set; } = string.Empty; ++ public string ExternalId { get; set; } = string.Empty; ++ public string LocalPath { get; set; } = string.Empty; ++ public string? LocalSubsonicId { get; set; } ++ public string Title { get; set; } = string.Empty; ++ public string Artist { get; set; } = string.Empty; ++ public string Album { get; set; } = string.Empty; ++ public DateTime DownloadedAt { get; set; } ++} +diff --git a/allstarr/Services/Qobuz/QobuzBundleService.cs b/allstarr/Services/Qobuz/QobuzBundleService.cs +new file mode 100644 +index 0000000..a15d60b +--- /dev/null ++++ b/allstarr/Services/Qobuz/QobuzBundleService.cs +@@ -0,0 +1,286 @@ ++using System.Text.RegularExpressions; ++ ++namespace allstarr.Services.Qobuz; ++ ++/// ++/// Service to dynamically extract Qobuz App ID and secrets from the Qobuz web player ++/// This is necessary because these values change periodically ++/// Based on the Python qobuz-dl implementation ++/// ++public class QobuzBundleService ++{ ++ private readonly HttpClient _httpClient; ++ private readonly ILogger _logger; ++ ++ private const string BaseUrl = "https://play.qobuz.com"; ++ private const string LoginPageUrl = "https://play.qobuz.com/login"; ++ ++ // Regex patterns to extract bundle URL and App ID ++ private static readonly Regex BundleUrlRegex = new( ++ @"", ++ RegexOptions.Compiled); ++ ++ private static readonly Regex AppIdRegex = new( ++ @"production:\{api:\{appId:""(?\d{9})"",appSecret:""\w{32}""", ++ RegexOptions.Compiled); ++ ++ // Cached values (valid for the lifetime of the application) ++ private string? _cachedAppId; ++ private List? _cachedSecrets; ++ private readonly SemaphoreSlim _initLock = new(1, 1); ++ ++ public QobuzBundleService(IHttpClientFactory httpClientFactory, ILogger logger) ++ { ++ _httpClient = httpClientFactory.CreateClient(); ++ _httpClient.DefaultRequestHeaders.Add("User-Agent", ++ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"); ++ _logger = logger; ++ } ++ ++ /// ++ /// Gets the Qobuz App ID, extracting it from the bundle if not cached ++ /// ++ public virtual async Task GetAppIdAsync() ++ { ++ await EnsureInitializedAsync(); ++ return _cachedAppId!; ++ } ++ ++ /// ++ /// Gets the Qobuz secrets list, extracting them from the bundle if not cached ++ /// ++ public virtual async Task> GetSecretsAsync() ++ { ++ await EnsureInitializedAsync(); ++ return _cachedSecrets!; ++ } ++ ++ /// ++ /// Gets a specific secret by index (used for signing requests) ++ /// ++ public virtual async Task GetSecretAsync(int index = 0) ++ { ++ var secrets = await GetSecretsAsync(); ++ if (index < 0 || index >= secrets.Count) ++ { ++ throw new ArgumentOutOfRangeException(nameof(index), ++ $"Secret index {index} out of range (0-{secrets.Count - 1})"); ++ } ++ return secrets[index]; ++ } ++ ++ /// ++ /// Ensures App ID and secrets are extracted and cached ++ /// ++ private async Task EnsureInitializedAsync() ++ { ++ if (_cachedAppId != null && _cachedSecrets != null) ++ { ++ return; ++ } ++ ++ await _initLock.WaitAsync(); ++ try ++ { ++ // Double-check after acquiring lock ++ if (_cachedAppId != null && _cachedSecrets != null) ++ { ++ return; ++ } ++ ++ _logger.LogInformation("Extracting Qobuz App ID and secrets from web bundle..."); ++ ++ // Step 1: Get the bundle URL from login page ++ var bundleUrl = await GetBundleUrlAsync(); ++ _logger.LogInformation("Found bundle URL: {BundleUrl}", bundleUrl); ++ ++ // Step 2: Download the bundle JavaScript ++ var bundleJs = await DownloadBundleAsync(bundleUrl); ++ ++ // Step 3: Extract App ID ++ _cachedAppId = ExtractAppId(bundleJs); ++ _logger.LogInformation("Extracted App ID: {AppId}", _cachedAppId); ++ ++ // Step 4: Extract secrets (they are base64 encoded in the bundle) ++ _cachedSecrets = ExtractSecrets(bundleJs); ++ _logger.LogInformation("Extracted {Count} secrets", _cachedSecrets.Count); ++ } ++ finally ++ { ++ _initLock.Release(); ++ } ++ } ++ ++ /// ++ /// Gets the bundle JavaScript URL from the login page ++ /// ++ private async Task GetBundleUrlAsync() ++ { ++ var response = await _httpClient.GetAsync(LoginPageUrl); ++ response.EnsureSuccessStatusCode(); ++ ++ var html = await response.Content.ReadAsStringAsync(); ++ var match = BundleUrlRegex.Match(html); ++ ++ if (!match.Success) ++ { ++ throw new Exception("Could not find bundle URL in Qobuz login page"); ++ } ++ ++ return BaseUrl + match.Groups[1].Value; ++ } ++ ++ /// ++ /// Downloads the bundle JavaScript file ++ /// ++ private async Task DownloadBundleAsync(string bundleUrl) ++ { ++ var response = await _httpClient.GetAsync(bundleUrl); ++ response.EnsureSuccessStatusCode(); ++ return await response.Content.ReadAsStringAsync(); ++ } ++ ++ /// ++ /// Extracts the App ID from the bundle JavaScript ++ /// ++ private string ExtractAppId(string bundleJs) ++ { ++ var match = AppIdRegex.Match(bundleJs); ++ ++ if (!match.Success) ++ { ++ throw new Exception("Could not extract App ID from bundle"); ++ } ++ ++ return match.Groups["app_id"].Value; ++ } ++ ++ /// ++ /// Extracts the secrets from the bundle JavaScript ++ /// Based on the Python qobuz-dl implementation (bundle.py) ++ /// The secrets are composed of seed, info, and extras base64-encoded strings ++ /// ++ private List ExtractSecrets(string bundleJs) ++ { ++ var secrets = new Dictionary>(); ++ ++ // Step 1: Extract seed and timezone pairs ++ // Pattern: [a-z].initialSeed("base64string",window.utimezone.timezone) ++ var seedTimezonePattern = new Regex( ++ @"[a-z]\.initialSeed\(""(?[\w=]+)"",window\.utimezone\.(?[a-z]+)\)", ++ RegexOptions.IgnoreCase); ++ ++ var seedMatches = seedTimezonePattern.Matches(bundleJs); ++ ++ foreach (Match match in seedMatches) ++ { ++ var seed = match.Groups["seed"].Value; ++ var timezone = match.Groups["timezone"].Value.ToLower(); ++ ++ if (!secrets.ContainsKey(timezone)) ++ { ++ secrets[timezone] = new List(); ++ } ++ secrets[timezone].Add(seed); ++ } ++ ++ if (secrets.Count == 0) ++ { ++ throw new Exception("Could not extract seed/timezone pairs from bundle"); ++ } ++ ++ // Step 2: Reorder secrets (move second item to first, as per Python implementation) ++ var keypairs = secrets.ToList(); ++ if (keypairs.Count > 1) ++ { ++ var secondItem = keypairs[1]; ++ secrets.Remove(secondItem.Key); ++ var newDict = new Dictionary> { { secondItem.Key, secondItem.Value } }; ++ foreach (var kv in keypairs) ++ { ++ if (kv.Key != secondItem.Key) ++ { ++ newDict[kv.Key] = kv.Value; ++ } ++ } ++ secrets = newDict; ++ } ++ ++ // Step 3: Extract info and extras for each timezone ++ // Pattern: name:"\w+/(Timezone)",info:"base64",extras:"base64" ++ var timezones = string.Join("|", secrets.Keys.Select(tz => ++ char.ToUpper(tz[0]) + tz.Substring(1))); ++ ++ var infoExtrasPattern = new Regex( ++ $@"name:""\w+/(?{timezones})"",info:""(?[\w=]+)"",extras:""(?[\w=]+)""", ++ RegexOptions.IgnoreCase); ++ ++ var infoExtrasMatches = infoExtrasPattern.Matches(bundleJs); ++ ++ foreach (Match match in infoExtrasMatches) ++ { ++ var timezone = match.Groups["timezone"].Value.ToLower(); ++ var info = match.Groups["info"].Value; ++ var extras = match.Groups["extras"].Value; ++ ++ if (secrets.ContainsKey(timezone)) ++ { ++ secrets[timezone].Add(info); ++ secrets[timezone].Add(extras); ++ } ++ } ++ ++ // Step 4: Decode the secrets ++ // Concatenate all base64 strings for each timezone, remove last 44 chars, then decode ++ var decodedSecrets = new List(); ++ ++ foreach (var kvp in secrets) ++ { ++ var concatenated = string.Join("", kvp.Value); ++ ++ // Remove last 44 characters as per Python implementation ++ if (concatenated.Length > 44) ++ { ++ concatenated = concatenated.Substring(0, concatenated.Length - 44); ++ } ++ ++ try ++ { ++ var bytes = Convert.FromBase64String(concatenated); ++ var decoded = System.Text.Encoding.UTF8.GetString(bytes); ++ decodedSecrets.Add(decoded); ++ _logger.LogDebug("Decoded secret for timezone {Timezone}: {Length} chars", kvp.Key, decoded.Length); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to decode secret for timezone {Timezone}", kvp.Key); ++ } ++ } ++ ++ if (decodedSecrets.Count == 0) ++ { ++ throw new Exception("Could not decode any secrets from bundle"); ++ } ++ ++ return decodedSecrets; ++ } ++ ++ /// ++ /// Tries to decode a base64 string ++ /// ++ private bool TryDecodeBase64(string input, out string decoded) ++ { ++ decoded = string.Empty; ++ ++ try ++ { ++ var bytes = Convert.FromBase64String(input); ++ decoded = System.Text.Encoding.UTF8.GetString(bytes); ++ return true; ++ } ++ catch ++ { ++ return false; ++ } ++ } ++} +diff --git a/allstarr/Services/Qobuz/QobuzDownloadService.cs b/allstarr/Services/Qobuz/QobuzDownloadService.cs +new file mode 100644 +index 0000000..5da212f +--- /dev/null ++++ b/allstarr/Services/Qobuz/QobuzDownloadService.cs +@@ -0,0 +1,328 @@ ++using System.Security.Cryptography; ++using System.Text; ++using System.Text.Json; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using allstarr.Services.Local; ++using allstarr.Services.Common; ++using allstarr.Services.Subsonic; ++using Microsoft.Extensions.Options; ++using IOFile = System.IO.File; ++ ++namespace allstarr.Services.Qobuz; ++ ++/// ++/// Download service implementation for Qobuz ++/// Handles track downloading with MD5 signature for authentication ++/// ++public class QobuzDownloadService : BaseDownloadService ++{ ++ private readonly HttpClient _httpClient; ++ private readonly QobuzBundleService _bundleService; ++ private readonly string? _userAuthToken; ++ private readonly string? _userId; ++ private readonly string? _preferredQuality; ++ ++ private const string BaseUrl = "https://www.qobuz.com/api.json/0.2/"; ++ ++ // Quality format IDs ++ private const int FormatMp3320 = 5; ++ private const int FormatFlac16 = 6; // CD quality (16-bit 44.1kHz) ++ private const int FormatFlac24Low = 7; // 24-bit < 96kHz ++ private const int FormatFlac24High = 27; // 24-bit >= 96kHz ++ ++ protected override string ProviderName => "qobuz"; ++ ++ public QobuzDownloadService( ++ IHttpClientFactory httpClientFactory, ++ IConfiguration configuration, ++ ILocalLibraryService localLibraryService, ++ IMusicMetadataService metadataService, ++ QobuzBundleService bundleService, ++ IOptions subsonicSettings, ++ IOptions qobuzSettings, ++ IServiceProvider serviceProvider, ++ ILogger logger) ++ : base(configuration, localLibraryService, metadataService, subsonicSettings.Value, serviceProvider, logger) ++ { ++ _httpClient = httpClientFactory.CreateClient(); ++ _bundleService = bundleService; ++ ++ var qobuzConfig = qobuzSettings.Value; ++ _userAuthToken = qobuzConfig.UserAuthToken; ++ _userId = qobuzConfig.UserId; ++ _preferredQuality = qobuzConfig.Quality; ++ } ++ ++ #region BaseDownloadService Implementation ++ ++ public override async Task IsAvailableAsync() ++ { ++ if (string.IsNullOrEmpty(_userAuthToken) || string.IsNullOrEmpty(_userId)) ++ { ++ Logger.LogWarning("Qobuz user auth token or user ID not configured"); ++ return false; ++ } ++ ++ try ++ { ++ await _bundleService.GetAppIdAsync(); ++ await _bundleService.GetSecretsAsync(); ++ return true; ++ } ++ catch (Exception ex) ++ { ++ Logger.LogWarning(ex, "Qobuz service not available"); ++ return false; ++ } ++ } ++ ++ protected override string? ExtractExternalIdFromAlbumId(string albumId) ++ { ++ const string prefix = "ext-qobuz-album-"; ++ if (albumId.StartsWith(prefix)) ++ { ++ return albumId[prefix.Length..]; ++ } ++ return null; ++ } ++ ++ protected override async Task DownloadTrackAsync(string trackId, Song song, CancellationToken cancellationToken) ++ { ++ // Get the download URL with signature ++ var downloadInfo = await GetTrackDownloadUrlAsync(trackId, cancellationToken); ++ ++ Logger.LogInformation("Download URL obtained for: {Title} - {Artist}", song.Title, song.Artist); ++ Logger.LogInformation("Quality: {BitDepth}bit/{SamplingRate}kHz, Format: {MimeType}", ++ downloadInfo.BitDepth, downloadInfo.SamplingRate, downloadInfo.MimeType); ++ ++ // Check if it's a demo/sample ++ if (downloadInfo.IsSample) ++ { ++ throw new Exception("Track is only available as a demo/sample"); ++ } ++ ++ // Determine extension based on MIME type ++ var extension = downloadInfo.MimeType?.Contains("flac") == true ? ".flac" : ".mp3"; ++ ++ // Build organized folder structure using AlbumArtist (fallback to Artist for singles) ++ var artistForPath = song.AlbumArtist ?? song.Artist; ++ var basePath = SubsonicSettings.StorageMode == StorageMode.Cache ? CachePath : DownloadPath; ++ var outputPath = PathHelper.BuildTrackPath(basePath, artistForPath, song.Album, song.Title, song.Track, extension); ++ ++ var albumFolder = Path.GetDirectoryName(outputPath)!; ++ EnsureDirectoryExists(albumFolder); ++ ++ outputPath = PathHelper.ResolveUniquePath(outputPath); ++ ++ // Download the file (Qobuz files are NOT encrypted like Deezer) ++ var response = await _httpClient.GetAsync(downloadInfo.Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); ++ response.EnsureSuccessStatusCode(); ++ ++ await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); ++ await using var outputFile = IOFile.Create(outputPath); ++ ++ await responseStream.CopyToAsync(outputFile, cancellationToken); ++ await outputFile.DisposeAsync(); ++ ++ // Write metadata and cover art ++ await WriteMetadataAsync(outputPath, song, cancellationToken); ++ ++ return outputPath; ++ } ++ ++ #endregion ++ ++ #region Qobuz Download Methods ++ ++ /// ++ /// Gets the download URL for a track with proper MD5 signature ++ /// ++ private async Task GetTrackDownloadUrlAsync(string trackId, CancellationToken cancellationToken) ++ { ++ var appId = await _bundleService.GetAppIdAsync(); ++ var secrets = await _bundleService.GetSecretsAsync(); ++ ++ if (secrets.Count == 0) ++ { ++ throw new Exception("No secrets available for signing"); ++ } ++ ++ // Determine format ID based on preferred quality ++ var formatId = GetFormatId(_preferredQuality); ++ ++ // Try the preferred quality first, then fallback to lower qualities ++ var formatPriority = GetFormatPriority(formatId); ++ ++ Exception? lastException = null; ++ ++ // Try each secret with each format ++ foreach (var secret in secrets) ++ { ++ var secretIndex = secrets.IndexOf(secret); ++ foreach (var format in formatPriority) ++ { ++ try ++ { ++ var result = await TryGetTrackDownloadUrlAsync(trackId, format, secret, cancellationToken); ++ ++ // Check if quality was downgraded ++ if (result.WasQualityDowngraded) ++ { ++ Logger.LogWarning("Requested quality not available, Qobuz downgraded to {BitDepth}bit/{SamplingRate}kHz", ++ result.BitDepth, result.SamplingRate); ++ } ++ ++ return result; ++ } ++ catch (Exception ex) ++ { ++ lastException = ex; ++ Logger.LogDebug("Failed to get download URL with secret {SecretIndex}, format {Format}: {Error}", ++ secretIndex, format, ex.Message); ++ } ++ } ++ } ++ ++ throw new Exception($"Failed to get download URL for all secrets and quality formats", lastException); ++ } ++ ++ private async Task TryGetTrackDownloadUrlAsync(string trackId, int formatId, string secret, CancellationToken cancellationToken) ++ { ++ var unix = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); ++ var appId = await _bundleService.GetAppIdAsync(); ++ var signature = ComputeMD5Signature(trackId, formatId, unix, secret); ++ ++ var url = $"{BaseUrl}track/getFileUrl?format_id={formatId}&intent=stream&request_ts={unix}&track_id={trackId}&request_sig={signature}"; ++ ++ using var request = new HttpRequestMessage(HttpMethod.Get, url); ++ ++ request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"); ++ request.Headers.Add("X-App-Id", appId); ++ ++ if (!string.IsNullOrEmpty(_userAuthToken)) ++ { ++ request.Headers.Add("X-User-Auth-Token", _userAuthToken); ++ } ++ ++ var response = await _httpClient.SendAsync(request, cancellationToken); ++ var responseBody = await response.Content.ReadAsStringAsync(cancellationToken); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ Logger.LogDebug("Qobuz getFileUrl failed - Status: {StatusCode}, TrackId: {TrackId}, FormatId: {FormatId}", ++ response.StatusCode, trackId, formatId); ++ throw new HttpRequestException($"Response status code does not indicate success: {response.StatusCode} ({response.ReasonPhrase})"); ++ } ++ ++ var doc = JsonDocument.Parse(responseBody); ++ var root = doc.RootElement; ++ ++ if (!root.TryGetProperty("url", out var urlElement) || string.IsNullOrEmpty(urlElement.GetString())) ++ { ++ throw new Exception("No download URL in response"); ++ } ++ ++ var downloadUrl = urlElement.GetString()!; ++ var mimeType = root.TryGetProperty("mime_type", out var mime) ? mime.GetString() : null; ++ var bitDepth = root.TryGetProperty("bit_depth", out var bd) ? bd.GetInt32() : 16; ++ var samplingRate = root.TryGetProperty("sampling_rate", out var sr) ? sr.GetDouble() : 44.1; ++ ++ var isSample = root.TryGetProperty("sample", out var sampleEl) && sampleEl.GetBoolean(); ++ if (samplingRate == 0) ++ { ++ isSample = true; ++ } ++ ++ var wasDowngraded = false; ++ if (root.TryGetProperty("restrictions", out var restrictions)) ++ { ++ foreach (var restriction in restrictions.EnumerateArray()) ++ { ++ if (restriction.TryGetProperty("code", out var code)) ++ { ++ var codeStr = code.GetString(); ++ if (codeStr == "FormatRestrictedByFormatAvailability") ++ { ++ wasDowngraded = true; ++ } ++ } ++ } ++ } ++ ++ return new QobuzDownloadResult ++ { ++ Url = downloadUrl, ++ FormatId = formatId, ++ MimeType = mimeType, ++ BitDepth = bitDepth, ++ SamplingRate = samplingRate, ++ IsSample = isSample, ++ WasQualityDowngraded = wasDowngraded ++ }; ++ } ++ ++ /// ++ /// Computes MD5 signature for track download request ++ /// ++ private string ComputeMD5Signature(string trackId, int formatId, long timestamp, string secret) ++ { ++ var toSign = $"trackgetFileUrlformat_id{formatId}intentstreamtrack_id{trackId}{timestamp}{secret}"; ++ ++ using var md5 = MD5.Create(); ++ var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(toSign)); ++ var signature = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); ++ ++ return signature; ++ } ++ ++ /// ++ /// Gets the format ID based on quality preference ++ /// ++ private int GetFormatId(string? quality) ++ { ++ if (string.IsNullOrEmpty(quality)) ++ { ++ return FormatFlac24High; ++ } ++ ++ return quality.ToUpperInvariant() switch ++ { ++ "FLAC" => FormatFlac24High, ++ "FLAC_24_HIGH" or "24_192" => FormatFlac24High, ++ "FLAC_24_LOW" or "24_96" => FormatFlac24Low, ++ "FLAC_16" or "CD" => FormatFlac16, ++ "MP3_320" or "MP3" => FormatMp3320, ++ _ => FormatFlac24High ++ }; ++ } ++ ++ /// ++ /// Gets the list of format IDs to try in priority order ++ /// ++ private List GetFormatPriority(int preferredFormat) ++ { ++ var allFormats = new List { FormatFlac24High, FormatFlac24Low, FormatFlac16, FormatMp3320 }; ++ ++ var priority = new List { preferredFormat }; ++ priority.AddRange(allFormats.Where(f => f != preferredFormat)); ++ ++ return priority; ++ } ++ ++ #endregion ++ ++ private class QobuzDownloadResult ++ { ++ public string Url { get; set; } = string.Empty; ++ public int FormatId { get; set; } ++ public string? MimeType { get; set; } ++ public int BitDepth { get; set; } ++ public double SamplingRate { get; set; } ++ public bool IsSample { get; set; } ++ public bool WasQualityDowngraded { get; set; } ++ } ++} +diff --git a/allstarr/Services/Qobuz/QobuzMetadataService.cs b/allstarr/Services/Qobuz/QobuzMetadataService.cs +new file mode 100644 +index 0000000..c718fe7 +--- /dev/null ++++ b/allstarr/Services/Qobuz/QobuzMetadataService.cs +@@ -0,0 +1,825 @@ ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using System.Text.Json; ++using Microsoft.Extensions.Options; ++ ++namespace allstarr.Services.Qobuz; ++ ++/// ++/// Metadata service implementation using the Qobuz API ++/// Uses user authentication token instead of email/password ++/// ++public class QobuzMetadataService : IMusicMetadataService ++{ ++ private readonly HttpClient _httpClient; ++ private readonly SubsonicSettings _settings; ++ private readonly QobuzBundleService _bundleService; ++ private readonly ILogger _logger; ++ private readonly string? _userAuthToken; ++ private readonly string? _userId; ++ ++ private const string BaseUrl = "https://www.qobuz.com/api.json/0.2/"; ++ ++ public QobuzMetadataService( ++ IHttpClientFactory httpClientFactory, ++ IOptions settings, ++ IOptions qobuzSettings, ++ QobuzBundleService bundleService, ++ ILogger logger) ++ { ++ _httpClient = httpClientFactory.CreateClient(); ++ _settings = settings.Value; ++ _bundleService = bundleService; ++ _logger = logger; ++ ++ var qobuzConfig = qobuzSettings.Value; ++ _userAuthToken = qobuzConfig.UserAuthToken; ++ _userId = qobuzConfig.UserId; ++ ++ // Set up default headers ++ _httpClient.DefaultRequestHeaders.Add("User-Agent", ++ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"); ++ } ++ ++ public async Task> SearchSongsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var appId = await _bundleService.GetAppIdAsync(); ++ var url = $"{BaseUrl}track/search?query={Uri.EscapeDataString(query)}&limit={limit}&app_id={appId}"; ++ ++ var response = await GetWithAuthAsync(url); ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var songs = new List(); ++ if (result.RootElement.TryGetProperty("tracks", out var tracks) && ++ tracks.TryGetProperty("items", out var items)) ++ { ++ foreach (var track in items.EnumerateArray()) ++ { ++ var song = ParseQobuzTrack(track); ++ if (ShouldIncludeSong(song)) ++ { ++ songs.Add(song); ++ } ++ } ++ } ++ ++ return songs; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to search songs for query: {Query}", query); ++ return new List(); ++ } ++ } ++ ++ public async Task> SearchAlbumsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var appId = await _bundleService.GetAppIdAsync(); ++ var url = $"{BaseUrl}album/search?query={Uri.EscapeDataString(query)}&limit={limit}&app_id={appId}"; ++ ++ var response = await GetWithAuthAsync(url); ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var albums = new List(); ++ if (result.RootElement.TryGetProperty("albums", out var albumsData) && ++ albumsData.TryGetProperty("items", out var items)) ++ { ++ foreach (var album in items.EnumerateArray()) ++ { ++ albums.Add(ParseQobuzAlbum(album)); ++ } ++ } ++ ++ return albums; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to search albums for query: {Query}", query); ++ return new List(); ++ } ++ } ++ ++ public async Task> SearchArtistsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var appId = await _bundleService.GetAppIdAsync(); ++ var url = $"{BaseUrl}artist/search?query={Uri.EscapeDataString(query)}&limit={limit}&app_id={appId}"; ++ ++ var response = await GetWithAuthAsync(url); ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var artists = new List(); ++ if (result.RootElement.TryGetProperty("artists", out var artistsData) && ++ artistsData.TryGetProperty("items", out var items)) ++ { ++ foreach (var artist in items.EnumerateArray()) ++ { ++ artists.Add(ParseQobuzArtist(artist)); ++ } ++ } ++ ++ return artists; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to search artists for query: {Query}", query); ++ return new List(); ++ } ++ } ++ ++ public async Task SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20) ++ { ++ var songsTask = SearchSongsAsync(query, songLimit); ++ var albumsTask = SearchAlbumsAsync(query, albumLimit); ++ var artistsTask = SearchArtistsAsync(query, artistLimit); ++ ++ await Task.WhenAll(songsTask, albumsTask, artistsTask); ++ ++ return new SearchResult ++ { ++ Songs = await songsTask, ++ Albums = await albumsTask, ++ Artists = await artistsTask ++ }; ++ } ++ ++ public async Task GetSongAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "qobuz") return null; ++ ++ try ++ { ++ var appId = await _bundleService.GetAppIdAsync(); ++ var url = $"{BaseUrl}track/get?track_id={externalId}&app_id={appId}"; ++ ++ var response = await GetWithAuthAsync(url); ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var track = JsonDocument.Parse(json).RootElement; ++ ++ if (track.TryGetProperty("error", out _)) return null; ++ ++ return ParseQobuzTrackFull(track); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to get song {ExternalId}", externalId); ++ return null; ++ } ++ } ++ ++ public async Task GetAlbumAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "qobuz") return null; ++ ++ try ++ { ++ var appId = await _bundleService.GetAppIdAsync(); ++ var url = $"{BaseUrl}album/get?album_id={externalId}&app_id={appId}"; ++ ++ var response = await GetWithAuthAsync(url); ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var albumElement = JsonDocument.Parse(json).RootElement; ++ ++ if (albumElement.TryGetProperty("error", out _)) return null; ++ ++ var album = ParseQobuzAlbum(albumElement); ++ ++ // Get album tracks ++ if (albumElement.TryGetProperty("tracks", out var tracks) && ++ tracks.TryGetProperty("items", out var tracksData)) ++ { ++ foreach (var track in tracksData.EnumerateArray()) ++ { ++ var song = ParseQobuzTrack(track); ++ ++ // Ensure album metadata is set (tracks in album response may not have full album object) ++ song.Album = album.Title; ++ song.AlbumId = album.Id; ++ song.AlbumArtist = album.Artist; ++ ++ if (ShouldIncludeSong(song)) ++ { ++ album.Songs.Add(song); ++ } ++ } ++ } ++ ++ return album; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to get album {ExternalId}", externalId); ++ return null; ++ } ++ } ++ ++ public async Task GetArtistAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "qobuz") return null; ++ ++ try ++ { ++ var appId = await _bundleService.GetAppIdAsync(); ++ var url = $"{BaseUrl}artist/get?artist_id={externalId}&app_id={appId}"; ++ ++ var response = await GetWithAuthAsync(url); ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var artist = JsonDocument.Parse(json).RootElement; ++ ++ if (artist.TryGetProperty("error", out _)) return null; ++ ++ return ParseQobuzArtist(artist); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to get artist {ExternalId}", externalId); ++ return null; ++ } ++ } ++ ++ public async Task> GetArtistAlbumsAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "qobuz") return new List(); ++ ++ try ++ { ++ var albums = new List(); ++ var appId = await _bundleService.GetAppIdAsync(); ++ int offset = 0; ++ const int limit = 500; ++ ++ // Qobuz requires pagination for artist albums ++ while (true) ++ { ++ var url = $"{BaseUrl}artist/get?artist_id={externalId}&app_id={appId}&limit={limit}&offset={offset}&extra=albums"; ++ ++ var response = await GetWithAuthAsync(url); ++ if (!response.IsSuccessStatusCode) break; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ if (!result.RootElement.TryGetProperty("albums", out var albumsData) || ++ !albumsData.TryGetProperty("items", out var items)) ++ { ++ break; ++ } ++ ++ var itemsArray = items.EnumerateArray().ToList(); ++ if (itemsArray.Count == 0) break; ++ ++ foreach (var album in itemsArray) ++ { ++ albums.Add(ParseQobuzAlbum(album)); ++ } ++ ++ // If we got less than the limit, we've reached the end ++ if (itemsArray.Count < limit) break; ++ ++ offset += limit; ++ } ++ ++ return albums; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to get artist albums for {ExternalId}", externalId); ++ return new List(); ++ } ++ } ++ ++ public async Task> SearchPlaylistsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var appId = await _bundleService.GetAppIdAsync(); ++ var url = $"{BaseUrl}playlist/search?query={Uri.EscapeDataString(query)}&limit={limit}&app_id={appId}"; ++ ++ var response = await GetWithAuthAsync(url); ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var playlists = new List(); ++ if (result.RootElement.TryGetProperty("playlists", out var playlistsData) && ++ playlistsData.TryGetProperty("items", out var items)) ++ { ++ foreach (var playlist in items.EnumerateArray()) ++ { ++ playlists.Add(ParseQobuzPlaylist(playlist)); ++ } ++ } ++ ++ return playlists; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to search playlists for query: {Query}", query); ++ return new List(); ++ } ++ } ++ ++ public async Task GetPlaylistAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "qobuz") return null; ++ ++ try ++ { ++ var appId = await _bundleService.GetAppIdAsync(); ++ var url = $"{BaseUrl}playlist/get?playlist_id={externalId}&app_id={appId}"; ++ ++ var response = await GetWithAuthAsync(url); ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var playlistElement = JsonDocument.Parse(json).RootElement; ++ ++ if (playlistElement.TryGetProperty("error", out _)) return null; ++ ++ return ParseQobuzPlaylist(playlistElement); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to get playlist {ExternalId}", externalId); ++ return null; ++ } ++ } ++ ++ public async Task> GetPlaylistTracksAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "qobuz") return new List(); ++ ++ try ++ { ++ var appId = await _bundleService.GetAppIdAsync(); ++ var url = $"{BaseUrl}playlist/get?playlist_id={externalId}&app_id={appId}&extra=tracks"; ++ ++ var response = await GetWithAuthAsync(url); ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var playlistElement = JsonDocument.Parse(json).RootElement; ++ ++ if (playlistElement.TryGetProperty("error", out _)) return new List(); ++ ++ var songs = new List(); ++ ++ // Get playlist name for album field ++ var playlistName = playlistElement.TryGetProperty("name", out var nameEl) ++ ? nameEl.GetString() ?? "Unknown Playlist" ++ : "Unknown Playlist"; ++ ++ if (playlistElement.TryGetProperty("tracks", out var tracks) && ++ tracks.TryGetProperty("items", out var tracksData)) ++ { ++ int trackIndex = 1; ++ foreach (var track in tracksData.EnumerateArray()) ++ { ++ // For playlists, use the track's own artist (not a single album artist) ++ var song = ParseQobuzTrack(track); ++ ++ // Override album name to be the playlist name ++ song.Album = playlistName; ++ song.Track = trackIndex; ++ ++ if (ShouldIncludeSong(song)) ++ { ++ songs.Add(song); ++ } ++ trackIndex++; ++ } ++ } ++ ++ return songs; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to get playlist tracks for {ExternalId}", externalId); ++ return new List(); ++ } ++ } ++ ++ private ExternalPlaylist ParseQobuzPlaylist(JsonElement playlist) ++ { ++ var externalId = GetIdAsString(playlist.GetProperty("id")); ++ ++ // Get curator/creator name ++ string? curatorName = null; ++ if (playlist.TryGetProperty("owner", out var owner) && ++ owner.TryGetProperty("name", out var ownerName)) ++ { ++ curatorName = ownerName.GetString(); ++ } ++ ++ // Get creation date ++ DateTime? createdDate = null; ++ if (playlist.TryGetProperty("created_at", out var createdAtEl)) ++ { ++ var timestamp = createdAtEl.GetInt64(); ++ createdDate = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; ++ } ++ ++ // Get cover URL from images ++ string? coverUrl = null; ++ if (playlist.TryGetProperty("images300", out var images300)) ++ { ++ var imagesArray = images300.EnumerateArray().ToList(); ++ if (imagesArray.Count > 0) ++ { ++ coverUrl = imagesArray[0].GetString(); ++ } ++ } ++ else if (playlist.TryGetProperty("image_rectangle", out var imageRect)) ++ { ++ var imagesArray = imageRect.EnumerateArray().ToList(); ++ if (imagesArray.Count > 0) ++ { ++ coverUrl = imagesArray[0].GetString(); ++ } ++ } ++ ++ return new ExternalPlaylist ++ { ++ Id = Common.PlaylistIdHelper.CreatePlaylistId("qobuz", externalId), ++ Name = playlist.TryGetProperty("name", out var name) ++ ? name.GetString() ?? "" ++ : "", ++ Description = playlist.TryGetProperty("description", out var desc) ++ ? desc.GetString() ++ : null, ++ CuratorName = curatorName, ++ Provider = "qobuz", ++ ExternalId = externalId, ++ TrackCount = playlist.TryGetProperty("tracks_count", out var tracksCount) ++ ? tracksCount.GetInt32() ++ : 0, ++ Duration = playlist.TryGetProperty("duration", out var duration) ++ ? duration.GetInt32() ++ : 0, ++ CoverUrl = coverUrl, ++ CreatedDate = createdDate ++ }; ++ } ++ ++ /// ++ /// Safely gets an ID value as a string, handling both number and string types from JSON ++ /// ++ private string GetIdAsString(JsonElement element) ++ { ++ return element.ValueKind switch ++ { ++ JsonValueKind.Number => element.GetInt64().ToString(), ++ JsonValueKind.String => element.GetString() ?? "", ++ _ => "" ++ }; ++ } ++ ++ /// ++ /// Makes an HTTP GET request with Qobuz authentication headers ++ /// ++ private async Task GetWithAuthAsync(string url) ++ { ++ using var request = new HttpRequestMessage(HttpMethod.Get, url); ++ ++ var appId = await _bundleService.GetAppIdAsync(); ++ request.Headers.Add("X-App-Id", appId); ++ ++ if (!string.IsNullOrEmpty(_userAuthToken)) ++ { ++ request.Headers.Add("X-User-Auth-Token", _userAuthToken); ++ } ++ ++ return await _httpClient.SendAsync(request); ++ } ++ ++ private Song ParseQobuzTrack(JsonElement track) ++ { ++ var externalId = GetIdAsString(track.GetProperty("id")); ++ ++ var title = track.GetProperty("title").GetString() ?? ""; ++ ++ // Add version to title if present (e.g., "Remastered", "Live") ++ if (track.TryGetProperty("version", out var version)) ++ { ++ var versionStr = version.GetString(); ++ if (!string.IsNullOrEmpty(versionStr)) ++ { ++ title = $"{title} ({versionStr})"; ++ } ++ } ++ ++ // For classical music, prepend work name ++ if (track.TryGetProperty("work", out var work)) ++ { ++ var workStr = work.GetString(); ++ if (!string.IsNullOrEmpty(workStr)) ++ { ++ title = $"{workStr}: {title}"; ++ } ++ } ++ ++ var performerName = track.TryGetProperty("performer", out var performer) ++ ? performer.GetProperty("name").GetString() ?? "" ++ : ""; ++ ++ var albumTitle = track.TryGetProperty("album", out var album) ++ ? album.GetProperty("title").GetString() ?? "" ++ : ""; ++ ++ var albumId = track.TryGetProperty("album", out var albumForId) ++ ? $"ext-qobuz-album-{GetIdAsString(albumForId.GetProperty("id"))}" ++ : null; ++ ++ // Get album artist ++ var albumArtist = track.TryGetProperty("album", out var albumForArtist) && ++ albumForArtist.TryGetProperty("artist", out var albumArtistEl) ++ ? albumArtistEl.GetProperty("name").GetString() ++ : performerName; ++ ++ return new Song ++ { ++ Id = $"ext-qobuz-song-{externalId}", ++ Title = title, ++ Artist = performerName, ++ ArtistId = track.TryGetProperty("performer", out var performerForId) ++ ? $"ext-qobuz-artist-{GetIdAsString(performerForId.GetProperty("id"))}" ++ : null, ++ Album = albumTitle, ++ AlbumId = albumId, ++ AlbumArtist = albumArtist, ++ Duration = track.TryGetProperty("duration", out var duration) ++ ? duration.GetInt32() ++ : null, ++ Track = track.TryGetProperty("track_number", out var trackNum) ++ ? trackNum.GetInt32() ++ : null, ++ DiscNumber = track.TryGetProperty("media_number", out var mediaNum) ++ ? mediaNum.GetInt32() ++ : null, ++ CoverArtUrl = GetCoverArtUrl(track), ++ IsLocal = false, ++ ExternalProvider = "qobuz", ++ ExternalId = externalId ++ }; ++ } ++ ++ private Song ParseQobuzTrackFull(JsonElement track) ++ { ++ var song = ParseQobuzTrack(track); ++ ++ // Add additional metadata for full track ++ if (track.TryGetProperty("composer", out var composer) && ++ composer.TryGetProperty("name", out var composerName)) ++ { ++ song.Contributors = new List { composerName.GetString() ?? "" }; ++ } ++ ++ if (track.TryGetProperty("isrc", out var isrc)) ++ { ++ song.Isrc = isrc.GetString(); ++ } ++ ++ if (track.TryGetProperty("copyright", out var copyright)) ++ { ++ song.Copyright = FormatCopyright(copyright.GetString() ?? ""); ++ } ++ ++ // Get release date from album ++ if (track.TryGetProperty("album", out var album)) ++ { ++ if (album.TryGetProperty("release_date_original", out var releaseDate)) ++ { ++ var dateStr = releaseDate.GetString(); ++ song.ReleaseDate = dateStr; ++ ++ if (!string.IsNullOrEmpty(dateStr) && dateStr.Length >= 4) ++ { ++ if (int.TryParse(dateStr.Substring(0, 4), out var year)) ++ { ++ song.Year = year; ++ } ++ } ++ } ++ ++ if (album.TryGetProperty("tracks_count", out var tracksCount)) ++ { ++ song.TotalTracks = tracksCount.GetInt32(); ++ } ++ ++ if (album.TryGetProperty("genres_list", out var genres)) ++ { ++ song.Genre = FormatGenres(genres); ++ } ++ ++ // Get large cover art ++ song.CoverArtUrlLarge = GetLargeCoverArtUrl(album); ++ } ++ ++ return song; ++ } ++ ++ private Album ParseQobuzAlbum(JsonElement album) ++ { ++ var externalId = GetIdAsString(album.GetProperty("id")); ++ ++ var title = album.GetProperty("title").GetString() ?? ""; ++ ++ // Add version to title if present ++ if (album.TryGetProperty("version", out var version)) ++ { ++ var versionStr = version.GetString(); ++ if (!string.IsNullOrEmpty(versionStr)) ++ { ++ title = $"{title} ({versionStr})"; ++ } ++ } ++ ++ var artistName = album.TryGetProperty("artist", out var artist) ++ ? artist.GetProperty("name").GetString() ?? "" ++ : ""; ++ ++ int? year = null; ++ if (album.TryGetProperty("release_date_original", out var releaseDate)) ++ { ++ var dateStr = releaseDate.GetString(); ++ if (!string.IsNullOrEmpty(dateStr) && dateStr.Length >= 4) ++ { ++ if (int.TryParse(dateStr.Substring(0, 4), out var y)) ++ { ++ year = y; ++ } ++ } ++ } ++ ++ return new Album ++ { ++ Id = $"ext-qobuz-album-{externalId}", ++ Title = title, ++ Artist = artistName, ++ ArtistId = album.TryGetProperty("artist", out var artistForId) ++ ? $"ext-qobuz-artist-{GetIdAsString(artistForId.GetProperty("id"))}" ++ : null, ++ Year = year, ++ SongCount = album.TryGetProperty("tracks_count", out var tracksCount) ++ ? tracksCount.GetInt32() ++ : null, ++ CoverArtUrl = GetCoverArtUrl(album), ++ Genre = album.TryGetProperty("genres_list", out var genres) ++ ? FormatGenres(genres) ++ : null, ++ IsLocal = false, ++ ExternalProvider = "qobuz", ++ ExternalId = externalId ++ }; ++ } ++ ++ private Artist ParseQobuzArtist(JsonElement artist) ++ { ++ var externalId = GetIdAsString(artist.GetProperty("id")); ++ ++ return new Artist ++ { ++ Id = $"ext-qobuz-artist-{externalId}", ++ Name = artist.GetProperty("name").GetString() ?? "", ++ ImageUrl = GetArtistImageUrl(artist), ++ AlbumCount = artist.TryGetProperty("albums_count", out var albumsCount) ++ ? albumsCount.GetInt32() ++ : null, ++ IsLocal = false, ++ ExternalProvider = "qobuz", ++ ExternalId = externalId ++ }; ++ } ++ ++ /// ++ /// Extracts cover art URL from track or album element ++ /// ++ private string? GetCoverArtUrl(JsonElement element) ++ { ++ // For tracks, get album image ++ if (element.TryGetProperty("album", out var album)) ++ { ++ element = album; ++ } ++ ++ if (element.TryGetProperty("image", out var image)) ++ { ++ // Prefer thumbnail (230x230), fallback to small ++ if (image.TryGetProperty("thumbnail", out var thumbnail)) ++ { ++ return thumbnail.GetString(); ++ } ++ if (image.TryGetProperty("small", out var small)) ++ { ++ return small.GetString(); ++ } ++ } ++ ++ return null; ++ } ++ ++ /// ++ /// Gets large cover art URL (600x600 or original) ++ /// ++ private string? GetLargeCoverArtUrl(JsonElement album) ++ { ++ if (album.TryGetProperty("image", out var image) && ++ image.TryGetProperty("large", out var large)) ++ { ++ var url = large.GetString(); ++ // Replace _600.jpg with _org.jpg for original quality ++ return url?.Replace("_600.jpg", "_org.jpg"); ++ } ++ ++ return null; ++ } ++ ++ /// ++ /// Gets artist image URL ++ /// ++ private string? GetArtistImageUrl(JsonElement artist) ++ { ++ if (artist.TryGetProperty("image", out var image) && ++ image.TryGetProperty("large", out var large)) ++ { ++ return large.GetString(); ++ } ++ ++ return null; ++ } ++ ++ /// ++ /// Formats Qobuz genre list into a readable string ++ /// Example: ["Pop/Rock", "Pop/Rock→Rock"] becomes "Pop, Rock" ++ /// ++ private string FormatGenres(JsonElement genresList) ++ { ++ var genres = new List(); ++ ++ foreach (var genre in genresList.EnumerateArray()) ++ { ++ var genreStr = genre.GetString(); ++ if (!string.IsNullOrEmpty(genreStr)) ++ { ++ // Extract individual genres from paths like "Pop/Rock→Rock→Alternative" ++ var parts = genreStr.Split(new[] { '/', '→' }, StringSplitOptions.RemoveEmptyEntries); ++ foreach (var part in parts) ++ { ++ var trimmed = part.Trim(); ++ if (!genres.Contains(trimmed)) ++ { ++ genres.Add(trimmed); ++ } ++ } ++ } ++ } ++ ++ return string.Join(", ", genres); ++ } ++ ++ /// ++ /// Formats copyright string ++ /// Replaces (P) with ℗ and (C) with © ++ /// ++ private string FormatCopyright(string copyright) ++ { ++ return copyright ++ .Replace("(P)", "℗") ++ .Replace("(C)", "©"); ++ } ++ ++ /// ++ /// Determines whether a song should be included based on the explicit content filter setting ++ /// Note: Qobuz doesn't have the same explicit content tagging as Deezer, so this is a no-op for now ++ /// ++ private bool ShouldIncludeSong(Song song) ++ { ++ // Qobuz API doesn't expose explicit content flags in the same way as Deezer ++ // We could implement this in the future if needed ++ return true; ++ } ++} +diff --git a/allstarr/Services/Qobuz/QobuzStartupValidator.cs b/allstarr/Services/Qobuz/QobuzStartupValidator.cs +new file mode 100644 +index 0000000..1cccf7a +--- /dev/null ++++ b/allstarr/Services/Qobuz/QobuzStartupValidator.cs +@@ -0,0 +1,129 @@ ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++using allstarr.Services.Validation; ++ ++namespace allstarr.Services.Qobuz; ++ ++/// ++/// Validates Qobuz credentials at startup ++/// ++public class QobuzStartupValidator : BaseStartupValidator ++{ ++ private readonly IOptions _qobuzSettings; ++ ++ public override string ServiceName => "Qobuz"; ++ ++ public QobuzStartupValidator(IOptions qobuzSettings, HttpClient httpClient) ++ : base(httpClient) ++ { ++ _qobuzSettings = qobuzSettings; ++ } ++ ++ public override async Task ValidateAsync(CancellationToken cancellationToken) ++ { ++ var userAuthToken = _qobuzSettings.Value.UserAuthToken; ++ var userId = _qobuzSettings.Value.UserId; ++ var quality = _qobuzSettings.Value.Quality; ++ ++ Console.WriteLine(); ++ ++ if (string.IsNullOrWhiteSpace(userAuthToken)) ++ { ++ WriteStatus("Qobuz UserAuthToken", "NOT CONFIGURED", ConsoleColor.Red); ++ WriteDetail("Set the Qobuz__UserAuthToken environment variable"); ++ return ValidationResult.NotConfigured("Qobuz UserAuthToken not configured"); ++ } ++ ++ if (string.IsNullOrWhiteSpace(userId)) ++ { ++ WriteStatus("Qobuz UserId", "NOT CONFIGURED", ConsoleColor.Red); ++ WriteDetail("Set the Qobuz__UserId environment variable"); ++ return ValidationResult.NotConfigured("Qobuz UserId not configured"); ++ } ++ ++ WriteStatus("Qobuz UserAuthToken", MaskSecret(userAuthToken), ConsoleColor.Cyan); ++ WriteStatus("Qobuz UserId", userId, ConsoleColor.Cyan); ++ WriteStatus("Qobuz Quality", quality ?? "auto (highest available)", ConsoleColor.Cyan); ++ ++ // Validate token by calling Qobuz API ++ await ValidateQobuzTokenAsync(userAuthToken, userId, cancellationToken); ++ ++ return ValidationResult.Success("Qobuz validation completed"); ++ } ++ ++ private async Task ValidateQobuzTokenAsync(string userAuthToken, string userId, CancellationToken cancellationToken) ++ { ++ const string fieldName = "Qobuz credentials"; ++ ++ try ++ { ++ // First, get the app ID from bundle service (simple check) ++ var bundleUrl = "https://play.qobuz.com/login"; ++ var bundleResponse = await _httpClient.GetAsync(bundleUrl, cancellationToken); ++ ++ if (!bundleResponse.IsSuccessStatusCode) ++ { ++ WriteStatus(fieldName, "UNABLE TO VERIFY", ConsoleColor.Yellow); ++ WriteDetail("Could not fetch Qobuz app configuration"); ++ return; ++ } ++ ++ // Try to validate with a simple API call ++ // We'll use the user favorites endpoint which requires authentication ++ var appId = "798273057"; // Fallback app ID ++ var apiUrl = $"https://www.qobuz.com/api.json/0.2/favorite/getUserFavorites?user_id={userId}&app_id={appId}"; ++ ++ using var request = new HttpRequestMessage(HttpMethod.Get, apiUrl); ++ request.Headers.Add("X-App-Id", appId); ++ request.Headers.Add("X-User-Auth-Token", userAuthToken); ++ request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"); ++ ++ var response = await _httpClient.SendAsync(request, cancellationToken); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ // 401 means invalid token, other errors might be network issues ++ if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) ++ { ++ WriteStatus(fieldName, "INVALID", ConsoleColor.Red); ++ WriteDetail("Token is expired or invalid"); ++ } ++ else ++ { ++ WriteStatus(fieldName, $"HTTP {(int)response.StatusCode}", ConsoleColor.Yellow); ++ WriteDetail("Unable to verify credentials"); ++ } ++ return; ++ } ++ ++ var json = await response.Content.ReadAsStringAsync(cancellationToken); ++ ++ // If we got a successful response, credentials are valid ++ if (!string.IsNullOrEmpty(json) && !json.Contains("\"error\"")) ++ { ++ WriteStatus(fieldName, "VALID", ConsoleColor.Green); ++ WriteDetail($"User ID: {userId}"); ++ } ++ else ++ { ++ WriteStatus(fieldName, "INVALID", ConsoleColor.Red); ++ WriteDetail("Unexpected response from Qobuz"); ++ } ++ } ++ catch (TaskCanceledException) ++ { ++ WriteStatus(fieldName, "TIMEOUT", ConsoleColor.Yellow); ++ WriteDetail("Could not reach Qobuz within 10 seconds"); ++ } ++ catch (HttpRequestException ex) ++ { ++ WriteStatus(fieldName, "UNREACHABLE", ConsoleColor.Yellow); ++ WriteDetail(ex.Message); ++ } ++ catch (Exception ex) ++ { ++ WriteStatus(fieldName, "ERROR", ConsoleColor.Red); ++ WriteDetail(ex.Message); ++ } ++ } ++} +diff --git a/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs +new file mode 100644 +index 0000000..ec30a86 +--- /dev/null ++++ b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs +@@ -0,0 +1,238 @@ ++using System.Text; ++using System.Text.Json; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using allstarr.Services.Local; ++using allstarr.Services.Common; ++using Microsoft.Extensions.Options; ++using IOFile = System.IO.File; ++using Microsoft.Extensions.Logging; ++ ++namespace allstarr.Services.SquidWTF; ++ ++/// ++/// Handles track downloading from tidal.squid.wtf (no encryption, no auth required) ++/// Downloads are direct from Tidal's CDN via the squid.wtf proxy ++/// ++public class SquidWTFDownloadService : BaseDownloadService ++{ ++ private readonly HttpClient _httpClient; ++ private readonly SemaphoreSlim _requestLock = new(1, 1); ++ private readonly string? _preferredQuality; ++ private readonly SquidWTFSettings _squidwtfSettings; ++ ++ private DateTime _lastRequestTime = DateTime.MinValue; ++ private readonly int _minRequestIntervalMs = 200; ++ ++ private const string SquidWTFApiBase = "https://triton.squid.wtf"; ++ ++ protected override string ProviderName => "squidwtf"; ++ ++ public SquidWTFDownloadService( ++ IHttpClientFactory httpClientFactory, ++ IConfiguration configuration, ++ ILocalLibraryService localLibraryService, ++ IMusicMetadataService metadataService, ++ IOptions subsonicSettings, ++ IOptions SquidWTFSettings, ++ IServiceProvider serviceProvider, ++ ILogger logger) ++ : base(configuration, localLibraryService, metadataService, subsonicSettings.Value, serviceProvider, logger) ++ { ++ _httpClient = httpClientFactory.CreateClient(); ++ _squidwtfSettings = SquidWTFSettings.Value; ++ } ++ ++ #region BaseDownloadService Implementation ++ ++ public override async Task IsAvailableAsync() ++ { ++ try ++ { ++ // Test connectivity to triton.squid.wtf ++ var response = await _httpClient.GetAsync("https://triton.squid.wtf/"); ++ Console.WriteLine($"Response code from is available async: {response.IsSuccessStatusCode}"); ++ return response.IsSuccessStatusCode; ++ } ++ catch (Exception ex) ++ { ++ Logger.LogWarning(ex, "SquidWTF service not available"); ++ return false; ++ } ++ } ++ ++ protected override string? ExtractExternalIdFromAlbumId(string albumId) ++ { ++ const string prefix = "ext-squidwtf-album-"; ++ if (albumId.StartsWith(prefix)) ++ { ++ Console.WriteLine(albumId[prefix.Length..]); ++ return albumId[prefix.Length..]; ++ } ++ return null; ++ } ++ ++ protected override async Task DownloadTrackAsync(string trackId, Song song, CancellationToken cancellationToken) ++ { ++ var downloadInfo = await GetTrackDownloadInfoAsync(trackId, cancellationToken); ++ ++ Logger.LogInformation("Track token obtained: {Url}", downloadInfo.DownloadUrl); ++ Logger.LogInformation("Using format: {Format}", downloadInfo.MimeType); ++ ++ // Determine extension from MIME type ++ var extension = downloadInfo.MimeType?.ToLower() switch ++ { ++ "audio/flac" => ".flac", ++ "audio/mpeg" => ".mp3", ++ "audio/mp4" => ".m4a", ++ _ => ".flac" // Default to FLAC ++ }; ++ ++ // Build organized folder structure: Artist/Album/Track using AlbumArtist (fallback to Artist for singles) ++ var artistForPath = song.AlbumArtist ?? song.Artist; ++ var outputPath = PathHelper.BuildTrackPath(DownloadPath, artistForPath, song.Album, song.Title, song.Track, extension); ++ ++ // Create directories if they don't exist ++ var albumFolder = Path.GetDirectoryName(outputPath)!; ++ EnsureDirectoryExists(albumFolder); ++ ++ // Resolve unique path if file already exists ++ outputPath = PathHelper.ResolveUniquePath(outputPath); ++ ++ // Download from Tidal CDN (no authentication needed, token is in URL) ++ var response = await QueueRequestAsync(async () => ++ { ++ using var request = new HttpRequestMessage(HttpMethod.Get, downloadInfo.DownloadUrl); ++ request.Headers.Add("User-Agent", "Mozilla/5.0"); ++ request.Headers.Add("Accept", "*/*"); ++ ++ return await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); ++ }); ++ ++ response.EnsureSuccessStatusCode(); ++ ++ // Download directly (no decryption needed - squid.wtf handles everything) ++ await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); ++ await using var outputFile = IOFile.Create(outputPath); ++ ++ await responseStream.CopyToAsync(outputFile, cancellationToken); ++ ++ // Close file before writing metadata ++ await outputFile.DisposeAsync(); ++ ++ // Write metadata and cover art ++ await WriteMetadataAsync(outputPath, song, cancellationToken); ++ ++ return outputPath; ++ } ++ ++ #endregion ++ ++ #region SquidWTF API Methods ++ ++ private async Task GetTrackDownloadInfoAsync(string trackId, CancellationToken cancellationToken) ++ { ++ return await QueueRequestAsync(async () => ++ { ++ // Map quality settings to Tidal's quality levels ++ var quality = _squidwtfSettings.Quality?.ToUpperInvariant() switch ++ { ++ "FLAC" => "LOSSLESS", ++ "HI_RES" => "HI_RES_LOSSLESS", ++ "LOSSLESS" => "LOSSLESS", ++ "HIGH" => "HIGH", ++ "LOW" => "LOW", ++ _ => "LOSSLESS" // Default to lossless ++ }; ++ ++ // Use the triton.squid.wtf endpoint to get track download info ++ var url = $"https://triton.squid.wtf/track/?id={trackId}&quality={quality}"; ++ ++ Console.WriteLine($"%%%%%%%%%%%%%%%%%%% URL For downloads??: {url}"); ++ ++ var response = await _httpClient.GetAsync(url, cancellationToken); ++ response.EnsureSuccessStatusCode(); ++ ++ var json = await response.Content.ReadAsStringAsync(cancellationToken); ++ var doc = JsonDocument.Parse(json); ++ ++ if (!doc.RootElement.TryGetProperty("data", out var data)) ++ { ++ throw new Exception("Invalid response from triton.squid.wtf"); ++ } ++ ++ // Get the manifest (base64 encoded JSON containing the actual CDN URL) ++ var manifestBase64 = data.GetProperty("manifest").GetString() ++ ?? throw new Exception("No manifest in response"); ++ ++ // Decode the manifest ++ var manifestJson = Encoding.UTF8.GetString(Convert.FromBase64String(manifestBase64)); ++ var manifest = JsonDocument.Parse(manifestJson); ++ ++ // Extract the download URL from the manifest ++ if (!manifest.RootElement.TryGetProperty("urls", out var urls) || urls.GetArrayLength() == 0) ++ { ++ throw new Exception("No download URLs in manifest"); ++ } ++ ++ var downloadUrl = urls[0].GetString() ++ ?? throw new Exception("Download URL is null"); ++ ++ var mimeType = manifest.RootElement.TryGetProperty("mimeType", out var mimeTypeEl) ++ ? mimeTypeEl.GetString() ++ : "audio/flac"; ++ ++ var audioQuality = data.TryGetProperty("audioQuality", out var audioQualityEl) ++ ? audioQualityEl.GetString() ++ : "LOSSLESS"; ++ ++ Logger.LogDebug("Decoded manifest - URL: {Url}, MIME: {MimeType}, Quality: {Quality}", ++ downloadUrl, mimeType, audioQuality); ++ ++ return new DownloadResult ++ { ++ DownloadUrl = downloadUrl, ++ MimeType = mimeType ?? "audio/flac", ++ AudioQuality = audioQuality ?? "LOSSLESS" ++ }; ++ }); ++ } ++ ++ #endregion ++ ++ #region Utility Methods ++ ++ private async Task QueueRequestAsync(Func> action) ++ { ++ await _requestLock.WaitAsync(); ++ try ++ { ++ var now = DateTime.UtcNow; ++ var timeSinceLastRequest = (now - _lastRequestTime).TotalMilliseconds; ++ ++ if (timeSinceLastRequest < _minRequestIntervalMs) ++ { ++ await Task.Delay((int)(_minRequestIntervalMs - timeSinceLastRequest)); ++ } ++ ++ _lastRequestTime = DateTime.UtcNow; ++ return await action(); ++ } ++ finally ++ { ++ _requestLock.Release(); ++ } ++ } ++ ++ #endregion ++ ++ private class DownloadResult ++ { ++ public string DownloadUrl { get; set; } = string.Empty; ++ public string MimeType { get; set; } = string.Empty; ++ public string AudioQuality { get; set; } = string.Empty; ++ } ++} +\ No newline at end of file +diff --git a/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs b/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs +new file mode 100644 +index 0000000..506e5a7 +--- /dev/null ++++ b/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs +@@ -0,0 +1,869 @@ ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Download; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++using allstarr.Services.Common; ++using System.Text.Json; ++using System.Text; ++using Microsoft.Extensions.Options; ++using System.Text.Json.Nodes; ++ ++namespace allstarr.Services.SquidWTF; ++ ++/// ++/// Metadata service implementation using the SquidWTF API (free, no key required) ++/// ++ ++public class SquidWTFMetadataService : IMusicMetadataService ++{ ++ private readonly HttpClient _httpClient; ++ private readonly SubsonicSettings _settings; ++ private readonly ILogger _logger; ++ private readonly RedisCacheService _cache; ++ private const string BaseUrl = "https://triton.squid.wtf"; ++ ++ public SquidWTFMetadataService( ++ IHttpClientFactory httpClientFactory, ++ IOptions settings, ++ IOptions squidwtfSettings, ++ ILogger logger, ++ RedisCacheService cache) ++ { ++ _httpClient = httpClientFactory.CreateClient(); ++ _settings = settings.Value; ++ _logger = logger; ++ _cache = cache; ++ ++ // Set up default headers ++ _httpClient.DefaultRequestHeaders.Add("User-Agent", ++ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"); ++ } ++ ++ public async Task> SearchSongsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var url = $"{BaseUrl}/search/?s={Uri.EscapeDataString(query)}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ return new List(); ++ } ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var songs = new List(); ++ if (result.RootElement.TryGetProperty("data", out var data) && ++ data.TryGetProperty("items", out var items)) ++ { ++ int count = 0; ++ foreach (var track in items.EnumerateArray()) ++ { ++ if (count >= limit) break; ++ ++ var song = ParseTidalTrack(track); ++ songs.Add(song); ++ count++; ++ } ++ } ++ return songs; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to search songs for query: {Query}", query); ++ return new List(); ++ } ++ } ++ ++ public async Task> SearchAlbumsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var url = $"{BaseUrl}/search/?al={Uri.EscapeDataString(query)}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ return new List(); ++ } ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var albums = new List(); ++ if (result.RootElement.TryGetProperty("data", out var data) && ++ data.TryGetProperty("albums", out var albumsObj) && ++ albumsObj.TryGetProperty("items", out var items)) ++ { ++ int count = 0; ++ foreach (var album in items.EnumerateArray()) ++ { ++ if (count >= limit) break; ++ ++ albums.Add(ParseTidalAlbum(album)); ++ count++; ++ } ++ } ++ ++ return albums; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to search albums for query: {Query}", query); ++ return new List(); ++ } ++ } ++ ++ public async Task> SearchArtistsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var url = $"{BaseUrl}/search/?a={Uri.EscapeDataString(query)}"; ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ return new List(); ++ } ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var artists = new List(); ++ if (result.RootElement.TryGetProperty("data", out var data) && ++ data.TryGetProperty("artists", out var artistsObj) && ++ artistsObj.TryGetProperty("items", out var items)) ++ { ++ int count = 0; ++ foreach (var artist in items.EnumerateArray()) ++ { ++ if (count >= limit) break; ++ ++ artists.Add(ParseTidalArtist(artist)); ++ count++; ++ } ++ } ++ ++ return artists; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to search artists for query: {Query}", query); ++ return new List(); ++ } ++ } ++ ++ public async Task> SearchPlaylistsAsync(string query, int limit = 20) ++ { ++ try ++ { ++ var url = $"{BaseUrl}/search/?p={Uri.EscapeDataString(query)}"; ++ var response = await _httpClient.GetAsync(url); ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ var playlists = new List(); ++ if (result.RootElement.TryGetProperty("data", out var data) && ++ data.TryGetProperty("playlists", out var playlistObj) && ++ playlistObj.TryGetProperty("items", out var items)) ++ { ++ foreach(var playlist in items.EnumerateArray()) ++ { ++ playlists.Add(ParseTidalPlaylist(playlist)); ++ } ++ } ++ return playlists; ++ } ++ catch ++ { ++ return new List(); ++ } ++ ++ ++ } ++ ++ public async Task SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20) ++ { ++ // Execute searches in parallel ++ var songsTask = SearchSongsAsync(query, songLimit); ++ var albumsTask = SearchAlbumsAsync(query, albumLimit); ++ var artistsTask = SearchArtistsAsync(query, artistLimit); ++ ++ await Task.WhenAll(songsTask, albumsTask, artistsTask); ++ ++ var temp = new SearchResult ++ { ++ Songs = await songsTask, ++ Albums = await albumsTask, ++ Artists = await artistsTask ++ }; ++ ++ return temp; ++ } ++ ++ public async Task GetSongAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "squidwtf") return null; ++ ++ try ++ { ++ // Use the /info endpoint for full track metadata ++ var url = $"{BaseUrl}/info/?id={externalId}"; ++ ++ var response = await _httpClient.GetAsync(url); ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ if (!result.RootElement.TryGetProperty("data", out var track)) ++ return null; ++ ++ return ParseTidalTrackFull(track); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "GetSongAsync Exception"); ++ return null; ++ } ++ } ++ ++ public async Task GetAlbumAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "squidwtf") return null; ++ ++ // Try cache first ++ var cacheKey = $"squidwtf:album:{externalId}"; ++ var cached = await _cache.GetAsync(cacheKey); ++ if (cached != null) return cached; ++ ++ try ++ { ++ // Use the /info endpoint for full track metadata ++ var url = $"{BaseUrl}/album/?id={externalId}"; ++ ++ var response = await _httpClient.GetAsync(url); ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var result = JsonDocument.Parse(json); ++ ++ ++ if (!result.RootElement.TryGetProperty("data", out var albumElement)) ++ return null; ++ ++ var album = ParseTidalAlbum(albumElement); ++ ++ // Get album tracks ++ if (albumElement.TryGetProperty("items", out var tracks)) ++ { ++ foreach (var trackWrapper in tracks.EnumerateArray()) ++ { ++ if (trackWrapper.TryGetProperty("item", out var track)) ++ { ++ var song = ParseTidalTrack(track); ++ if (ShouldIncludeSong(song)) ++ { ++ album.Songs.Add(song); ++ } ++ } ++ } ++ } ++ ++ // Cache for 24 hours ++ await _cache.SetAsync(cacheKey, album, TimeSpan.FromHours(24)); ++ ++ return album; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "GetAlbumAsync Exception"); ++ return null; ++ } ++ } ++ ++ public async Task GetArtistAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "squidwtf") return null; ++ ++ _logger.LogInformation("GetArtistAsync called for SquidWTF artist {ExternalId}", externalId); ++ ++ // Try cache first ++ var cacheKey = $"squidwtf:artist:{externalId}"; ++ var cached = await _cache.GetAsync(cacheKey); ++ if (cached != null) ++ { ++ _logger.LogInformation("Returning cached artist {ArtistName}", cached.Name); ++ return cached; ++ } ++ ++ try ++ { ++ // Use the /info endpoint for full track metadata ++ var url = $"{BaseUrl}/artist/?f={externalId}"; ++ _logger.LogInformation("Fetching artist from {Url}", url); ++ ++ var response = await _httpClient.GetAsync(url); ++ if (!response.IsSuccessStatusCode) ++ { ++ _logger.LogWarning("SquidWTF artist request failed with status {StatusCode}", response.StatusCode); ++ return null; ++ } ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ _logger.LogDebug("SquidWTF artist response: {Json}", json.Length > 500 ? json.Substring(0, 500) + "..." : json); ++ var result = JsonDocument.Parse(json); ++ ++ JsonElement? artistSource = null; ++ int albumCount = 0; ++ ++ // Think this can maybe switch to something using ParseTidalAlbum ++ if (result.RootElement.TryGetProperty("albums", out var albums) && ++ albums.TryGetProperty("items", out var albumItems) && ++ albumItems.GetArrayLength() > 0) ++ { ++ albumCount = albumItems.GetArrayLength(); ++ artistSource = albumItems[0].GetProperty("artist"); ++ _logger.LogInformation("Found artist from albums, albumCount={AlbumCount}", albumCount); ++ } ++ ++ // Think this can maybe switch to something using ParseTidalTrack ++ else if (result.RootElement.TryGetProperty("tracks", out var tracks) && ++ tracks.GetArrayLength() > 0 && ++ tracks[0].TryGetProperty("artists", out var artists) && ++ artists.GetArrayLength() > 0) ++ { ++ artistSource = artists[0]; ++ _logger.LogInformation("Found artist from tracks"); ++ } ++ ++ if (artistSource == null) ++ { ++ _logger.LogWarning("Could not find artist data in response"); ++ return null; ++ } ++ ++ var artistElement = artistSource.Value; ++ var normalizedArtist = new JsonObject ++ { ++ ["id"] = artistElement.GetProperty("id").GetInt64(), ++ ["name"] = artistElement.GetProperty("name").GetString(), ++ ["albums_count"] = albumCount, ++ ["picture"] = artistElement.GetProperty("picture").GetString() ++ }; ++ ++ using var doc = JsonDocument.Parse(normalizedArtist.ToJsonString()); ++ var artist = ParseTidalArtist(doc.RootElement); ++ ++ _logger.LogInformation("Successfully parsed artist {ArtistName} with {AlbumCount} albums", artist.Name, albumCount); ++ ++ // Cache for 24 hours ++ await _cache.SetAsync(cacheKey, artist, TimeSpan.FromHours(24)); ++ ++ return artist; ++ ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "GetArtistAsync Exception."); ++ return null; ++ } ++ } ++ ++ public async Task> GetArtistAlbumsAsync(string externalProvider, string externalId) ++ { ++ ++ try ++ { ++ if (externalProvider != "squidwtf") return new List(); ++ ++ _logger.LogInformation("GetArtistAlbumsAsync called for SquidWTF artist {ExternalId}", externalId); ++ ++ var url = $"{BaseUrl}/artist/?f={externalId}"; ++ _logger.LogInformation("Fetching artist albums from URL: {Url}", url); ++ var response = await _httpClient.GetAsync(url); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ _logger.LogWarning("SquidWTF artist albums request failed with status {StatusCode}", response.StatusCode); ++ return new List(); ++ } ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ _logger.LogDebug("SquidWTF artist albums response for {ExternalId}: {JsonLength} bytes", externalId, json.Length); ++ var result = JsonDocument.Parse(json); ++ ++ var albums = new List(); ++ ++ if (result.RootElement.TryGetProperty("albums", out var albumsObj) && ++ albumsObj.TryGetProperty("items", out var items)) ++ { ++ foreach (var album in items.EnumerateArray()) ++ { ++ var parsedAlbum = ParseTidalAlbum(album); ++ _logger.LogInformation("Parsed album: {AlbumTitle} by {ArtistName} (ArtistId: {ArtistId})", ++ parsedAlbum.Title, parsedAlbum.Artist, parsedAlbum.ArtistId); ++ albums.Add(parsedAlbum); ++ } ++ _logger.LogInformation("Found {AlbumCount} albums for artist {ExternalId}", albums.Count, externalId); ++ } ++ else ++ { ++ _logger.LogWarning("No albums found in response for artist {ExternalId}", externalId); ++ } ++ ++ return albums; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to get SquidWTF artist albums for {ExternalId}", externalId); ++ return new List(); ++ } ++ } ++ ++ public async Task GetPlaylistAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "squidwtf") return null; ++ ++ try ++ { ++ var url = $"{BaseUrl}/playlist/?id={externalId}"; ++ var response = await _httpClient.GetAsync(url); ++ if (!response.IsSuccessStatusCode) return null; ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var playlistElement = JsonDocument.Parse(json).RootElement; ++ ++ if (playlistElement.TryGetProperty("error", out _)) return null; ++ ++ return ParseTidalPlaylist(playlistElement); ++ } ++ catch ++ { ++ return null; ++ } ++ ++ } ++ ++ public async Task> GetPlaylistTracksAsync(string externalProvider, string externalId) ++ { ++ if (externalProvider != "squidwtf") return new List(); ++ ++ try ++ { ++ var url = $"{BaseUrl}/playlist/?id={externalId}"; ++ var response = await _httpClient.GetAsync(url); ++ if (!response.IsSuccessStatusCode) return new List(); ++ ++ var json = await response.Content.ReadAsStringAsync(); ++ var playlistElement = JsonDocument.Parse(json).RootElement; ++ ++ if (playlistElement.TryGetProperty("error", out _)) return new List(); ++ ++ JsonElement? playlist = null; ++ JsonElement? tracks = null; ++ ++ if (playlistElement.TryGetProperty("playlist", out var playlistEl)) ++ { ++ playlist = playlistEl; ++ } ++ ++ if (playlistElement.TryGetProperty("items", out var tracksEl)) ++ { ++ tracks = tracksEl; ++ } ++ ++ var songs = new List(); ++ ++ // Get playlist name for album field ++ var playlistName = playlist.Value.TryGetProperty("title", out var titleEl) ++ ? titleEl.GetString() ?? "Unknown Playlist" ++ : "Unknown Playlist"; ++ ++ if (tracks != null) ++ { ++ int trackIndex = 1; ++ foreach (var entry in tracks.Value.EnumerateArray()) ++ { ++ if (!entry.TryGetProperty("item", out var track)) ++ continue; ++ ++ // For playlists, use the track's own artist (not a single album artist) ++ var song = ParseTidalTrack(track, trackIndex); ++ ++ // Override album name to be the playlist name ++ song.Album = playlistName; ++ ++ if (ShouldIncludeSong(song)) ++ { ++ songs.Add(song); ++ } ++ trackIndex++; ++ } ++ } ++ return songs; ++ } ++ catch ++ { ++ return new List(); ++ } ++ ++ } ++ ++ // --- Parser functions start here --- ++ ++ private Song ParseTidalTrack(JsonElement track, int? fallbackTrackNumber = null) ++ { ++ var externalId = track.GetProperty("id").GetInt64().ToString(); ++ ++ // Explicit content lyrics value - idk if this will work ++ int? explicitContentLyrics = ++ track.TryGetProperty("explicit", out var ecl) && ecl.ValueKind == JsonValueKind.True ++ ? 1 ++ : 0; ++ ++ int? trackNumber = track.TryGetProperty("trackNumber", out var trackNum) ++ ? trackNum.GetInt32() ++ : fallbackTrackNumber; ++ ++ int? discNumber = track.TryGetProperty("volumeNumber", out var volNum) ++ ? volNum.GetInt32() ++ : null; ++ ++ // Get artist name - handle both single artist and artists array ++ string artistName = ""; ++ if (track.TryGetProperty("artist", out var artist)) ++ { ++ artistName = artist.GetProperty("name").GetString() ?? ""; ++ } ++ else if (track.TryGetProperty("artists", out var artists) && artists.GetArrayLength() > 0) ++ { ++ artistName = artists[0].GetProperty("name").GetString() ?? ""; ++ } ++ ++ // Get artist ID ++ string? artistId = null; ++ if (track.TryGetProperty("artist", out var artistForId)) ++ { ++ artistId = $"ext-squidwtf-artist-{artistForId.GetProperty("id").GetInt64()}"; ++ } ++ else if (track.TryGetProperty("artists", out var artistsForId) && artistsForId.GetArrayLength() > 0) ++ { ++ artistId = $"ext-squidwtf-artist-{artistsForId[0].GetProperty("id").GetInt64()}"; ++ } ++ ++ // Get album info ++ string albumTitle = ""; ++ string? albumId = null; ++ string? coverArt = null; ++ ++ if (track.TryGetProperty("album", out var album)) ++ { ++ albumTitle = album.GetProperty("title").GetString() ?? ""; ++ albumId = $"ext-squidwtf-album-{album.GetProperty("id").GetInt64()}"; ++ ++ if (album.TryGetProperty("cover", out var cover)) ++ { ++ var coverGuid = cover.GetString()?.Replace("-", "/"); ++ coverArt = $"https://resources.tidal.com/images/{coverGuid}/320x320.jpg"; ++ } ++ } ++ ++ return new Song ++ { ++ Id = $"ext-squidwtf-song-{externalId}", ++ Title = track.GetProperty("title").GetString() ?? "", ++ Artist = artistName, ++ ArtistId = artistId, ++ Album = albumTitle, ++ AlbumId = albumId, ++ Duration = track.TryGetProperty("duration", out var duration) ++ ? duration.GetInt32() ++ : null, ++ Track = trackNumber, ++ DiscNumber = discNumber, ++ CoverArtUrl = coverArt, ++ IsLocal = false, ++ ExternalProvider = "squidwtf", ++ ExternalId = externalId, ++ ExplicitContentLyrics = explicitContentLyrics ++ }; ++ } ++ ++ private Song ParseTidalTrackFull(JsonElement track) ++ { ++ var externalId = track.GetProperty("id").GetInt64().ToString(); ++ ++ // Explicit content lyrics value - idk if this will work ++ int? explicitContentLyrics = ++ track.TryGetProperty("explicit", out var ecl) && ecl.ValueKind == JsonValueKind.True ++ ? 1 ++ : 0; ++ ++ ++ int? trackNumber = track.TryGetProperty("trackNumber", out var trackNum) ++ ? trackNum.GetInt32() ++ : null; ++ ++ int? discNumber = track.TryGetProperty("volumeNumber", out var volNum) ++ ? volNum.GetInt32() ++ : null; ++ ++ int? bpm = track.TryGetProperty("bpm", out var bpmVal) && bpmVal.ValueKind == JsonValueKind.Number ++ ? bpmVal.GetInt32() ++ : null; ++ ++ string? isrc = track.TryGetProperty("isrc", out var isrcVal) ++ ? isrcVal.GetString() ++ : null; ++ ++ int? year = null; ++ if (track.TryGetProperty("streamStartDate", out var streamDate)) ++ { ++ var dateStr = streamDate.GetString(); ++ if (!string.IsNullOrEmpty(dateStr) && dateStr.Length >= 4) ++ { ++ if (int.TryParse(dateStr.Substring(0, 4), out var y)) ++ year = y; ++ } ++ } ++ ++ // Get artist info ++ string artistName = track.GetProperty("artist").GetProperty("name").GetString() ?? ""; ++ long artistIdNum = track.GetProperty("artist").GetProperty("id").GetInt64(); ++ ++ // Album artist - same as main artist for Tidal tracks ++ string? albumArtist = artistName; ++ ++ // Get album info ++ var album = track.GetProperty("album"); ++ string albumTitle = album.GetProperty("title").GetString() ?? ""; ++ long albumIdNum = album.GetProperty("id").GetInt64(); ++ ++ // Cover art URLs ++ string? coverArt = null; ++ string? coverArtLarge = null; ++ if (album.TryGetProperty("cover", out var cover)) ++ { ++ var coverGuid = cover.GetString()?.Replace("-", "/"); ++ coverArt = $"https://resources.tidal.com/images/{coverGuid}/320x320.jpg"; ++ coverArtLarge = $"https://resources.tidal.com/images/{coverGuid}/1280x1280.jpg"; ++ } ++ ++ // Copyright ++ string? copyright = track.TryGetProperty("copyright", out var copyrightVal) ++ ? copyrightVal.GetString() ++ : null; ++ ++ // Explicit content ++ bool isExplicit = track.TryGetProperty("explicit", out var explicitVal) && explicitVal.GetBoolean(); ++ ++ return new Song ++ { ++ Id = $"ext-squidwtf-song-{externalId}", ++ Title = track.GetProperty("title").GetString() ?? "", ++ Artist = artistName, ++ ArtistId = $"ext-squidwtf-artist-{artistIdNum}", ++ Album = albumTitle, ++ AlbumId = $"ext-squidwtf-album-{albumIdNum}", ++ AlbumArtist = albumArtist, ++ Duration = track.TryGetProperty("duration", out var duration) ++ ? duration.GetInt32() ++ : null, ++ Track = trackNumber, ++ DiscNumber = discNumber, ++ Year = year, ++ Bpm = bpm, ++ Isrc = isrc, ++ CoverArtUrl = coverArt, ++ CoverArtUrlLarge = coverArtLarge, ++ Label = copyright, // Store copyright in label field ++ IsLocal = false, ++ ExternalProvider = "squidwtf", ++ ExternalId = externalId, ++ ExplicitContentLyrics = explicitContentLyrics ++ }; ++ } ++ ++ private Album ParseTidalAlbum(JsonElement album) ++ { ++ var externalId = album.GetProperty("id").GetInt64().ToString(); ++ ++ int? year = null; ++ if (album.TryGetProperty("releaseDate", out var releaseDate)) ++ { ++ var dateStr = releaseDate.GetString(); ++ if (!string.IsNullOrEmpty(dateStr) && dateStr.Length >= 4) ++ { ++ if (int.TryParse(dateStr.Substring(0, 4), out var y)) ++ year = y; ++ } ++ } ++ ++ string? coverArt = null; ++ if (album.TryGetProperty("cover", out var cover)) ++ { ++ var coverGuid = cover.GetString()?.Replace("-", "/"); ++ coverArt = $"https://resources.tidal.com/images/{coverGuid}/320x320.jpg"; ++ } ++ ++ // Get artist name ++ string artistName = ""; ++ string? artistId = null; ++ if (album.TryGetProperty("artist", out var artist)) ++ { ++ artistName = artist.GetProperty("name").GetString() ?? ""; ++ artistId = $"ext-squidwtf-artist-{artist.GetProperty("id").GetInt64()}"; ++ } ++ else if (album.TryGetProperty("artists", out var artists) && artists.GetArrayLength() > 0) ++ { ++ artistName = artists[0].GetProperty("name").GetString() ?? ""; ++ artistId = $"ext-squidwtf-artist-{artists[0].GetProperty("id").GetInt64()}"; ++ } ++ ++ return new Album ++ { ++ Id = $"ext-squidwtf-album-{externalId}", ++ Title = album.GetProperty("title").GetString() ?? "", ++ Artist = artistName, ++ ArtistId = artistId, ++ Year = year, ++ SongCount = album.TryGetProperty("numberOfTracks", out var trackCount) ++ ? trackCount.GetInt32() ++ : null, ++ CoverArtUrl = coverArt, ++ IsLocal = false, ++ ExternalProvider = "squidwtf", ++ ExternalId = externalId ++ }; ++ } ++ ++ // TODO: Think of a way to implement album count when this function is called by search function ++ // as the API endpoint in search does not include this data ++ private Artist ParseTidalArtist(JsonElement artist) ++ { ++ var externalId = artist.GetProperty("id").GetInt64().ToString(); ++ ++ string? imageUrl = null; ++ if (artist.TryGetProperty("picture", out var picture)) ++ { ++ var pictureGuid = picture.GetString()?.Replace("-", "/"); ++ imageUrl = $"https://resources.tidal.com/images/{pictureGuid}/320x320.jpg"; ++ } ++ ++ return new Artist ++ { ++ Id = $"ext-squidwtf-artist-{externalId}", ++ Name = artist.GetProperty("name").GetString() ?? "", ++ ImageUrl = imageUrl, ++ AlbumCount = artist.TryGetProperty("albums_count", out var albumsCount) ++ ? albumsCount.GetInt32() ++ : null, ++ IsLocal = false, ++ ExternalProvider = "squidwtf", ++ ExternalId = externalId ++ }; ++ } ++ ++ private ExternalPlaylist ParseTidalPlaylist(JsonElement playlistElement) ++ { ++ JsonElement? playlist = null; ++ JsonElement? tracks = null; ++ ++ if (playlistElement.TryGetProperty("playlist", out var playlistEl)) ++ { ++ playlist = playlistEl; ++ } ++ ++ if (playlistElement.TryGetProperty("items", out var tracksEl)) ++ { ++ tracks = tracksEl; ++ } ++ ++ var externalId = playlist.Value.GetProperty("uuid").GetString()!; ++ ++ // Get curator/creator name ++ string? curatorName = null; ++ if (playlist.Value.TryGetProperty("creator", out var creator) && ++ creator.TryGetProperty("id", out var id)) ++ { ++ curatorName = id.GetString(); ++ } ++ ++ // Get creation date ++ DateTime? createdDate = null; ++ if (playlist.Value.TryGetProperty("created", out var creationDateEl)) ++ { ++ var dateStr = creationDateEl.GetString(); ++ if (!string.IsNullOrEmpty(dateStr) && DateTime.TryParse(dateStr, out var date)) ++ { ++ createdDate = date; ++ } ++ } ++ ++ // Get playlist image URL ++ string? imageUrl = null; ++ if (playlist.Value.TryGetProperty("squareImage", out var picture)) ++ { ++ var pictureGuid = picture.GetString()?.Replace("-", "/"); ++ imageUrl = $"https://resources.tidal.com/images/{pictureGuid}/1080x1080.jpg"; ++ // Maybe later add support for potentential fallbacks if this size isn't available ++ } ++ ++ return new ExternalPlaylist ++ { ++ Id = Common.PlaylistIdHelper.CreatePlaylistId("squidwtf", externalId), ++ Name = playlist.Value.GetProperty("title").GetString() ?? "", ++ Description = playlist.Value.TryGetProperty("description", out var desc) ++ ? desc.GetString() ++ : null, ++ CuratorName = curatorName, ++ Provider = "squidwtf", ++ ExternalId = externalId, ++ TrackCount = playlist.Value.TryGetProperty("numberOfTracks", out var nbTracks) ++ ? nbTracks.GetInt32() ++ : 0, ++ Duration = playlist.Value.TryGetProperty("duration", out var duration) ++ ? duration.GetInt32() ++ : 0, ++ CoverUrl = imageUrl, ++ CreatedDate = createdDate ++ }; ++ ++ } ++ ++ /// ++ /// Determines whether a song should be included based on the explicit content filter setting ++ /// ++ /// The song to check ++ /// True if the song should be included, false otherwise ++ private bool ShouldIncludeSong(Song song) ++ { ++ // If no explicit content info, include the song ++ if (song.ExplicitContentLyrics == null) ++ return true; ++ ++ return _settings.ExplicitFilter switch ++ { ++ // All: No filtering, include everything ++ ExplicitFilter.All => true, ++ ++ // ExplicitOnly: Exclude clean/edited versions (value 3) ++ // Include: 0 (naturally clean), 1 (explicit), 2 (not applicable), 6/7 (unknown) ++ ExplicitFilter.ExplicitOnly => song.ExplicitContentLyrics != 3, ++ ++ // CleanOnly: Only show clean content ++ // Include: 0 (naturally clean), 3 (clean/edited version) ++ // Exclude: 1 (explicit) ++ ExplicitFilter.CleanOnly => song.ExplicitContentLyrics != 1, ++ ++ _ => true ++ }; ++ } ++ ++} +\ No newline at end of file +diff --git a/allstarr/Services/SquidWTF/SquidWTFStartupValidator.cs b/allstarr/Services/SquidWTF/SquidWTFStartupValidator.cs +new file mode 100644 +index 0000000..418859a +--- /dev/null ++++ b/allstarr/Services/SquidWTF/SquidWTFStartupValidator.cs +@@ -0,0 +1,118 @@ ++using System.Text; ++using System.Text.Json; ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++using allstarr.Services.Validation; ++ ++namespace allstarr.Services.SquidWTF; ++ ++/// ++/// Validates SquidWTF service connectivity at startup (no auth needed) ++/// ++public class SquidWTFStartupValidator : BaseStartupValidator ++{ ++ private readonly SquidWTFSettings _settings; ++ ++ public override string ServiceName => "SquidWTF"; ++ ++ public SquidWTFStartupValidator(IOptions settings, HttpClient httpClient) ++ : base(httpClient) ++ { ++ _settings = settings.Value; ++ } ++ ++ public override async Task ValidateAsync(CancellationToken cancellationToken) ++ { ++ Console.WriteLine(); ++ ++ var quality = _settings.Quality?.ToUpperInvariant() switch ++ { ++ "FLAC" => "LOSSLESS", ++ "HI_RES" => "HI_RES_LOSSLESS", ++ "LOSSLESS" => "LOSSLESS", ++ "HIGH" => "HIGH", ++ "LOW" => "LOW", ++ _ => "LOSSLESS (default)" ++ }; ++ ++ WriteStatus("SquidWTF Quality", quality, ConsoleColor.Cyan); ++ ++ // Test connectivity to triton.squid.wtf ++ try ++ { ++ var response = await _httpClient.GetAsync("https://triton.squid.wtf/", cancellationToken); ++ ++ if (response.IsSuccessStatusCode) ++ { ++ WriteStatus("SquidWTF API", "REACHABLE", ConsoleColor.Green); ++ WriteDetail("No authentication required - powered by Tidal"); ++ ++ // Try a test search to verify functionality ++ await ValidateSearchFunctionality(cancellationToken); ++ ++ return ValidationResult.Success("SquidWTF validation completed"); ++ } ++ else ++ { ++ WriteStatus("SquidWTF API", $"HTTP {(int)response.StatusCode}", ConsoleColor.Yellow); ++ WriteDetail("Service may be temporarily unavailable"); ++ return ValidationResult.Failure($"{response.StatusCode}", "SquidWTF returned code"); ++ } ++ } ++ catch (TaskCanceledException) ++ { ++ WriteStatus("SquidWTF API", "TIMEOUT", ConsoleColor.Yellow); ++ WriteDetail("Could not reach service within timeout period"); ++ return ValidationResult.Failure("-1", "SquidWTF connection timeout"); ++ } ++ catch (HttpRequestException ex) ++ { ++ WriteStatus("SquidWTF API", "UNREACHABLE", ConsoleColor.Red); ++ WriteDetail(ex.Message); ++ return ValidationResult.Failure("-1", $"Cannot connect to SquidWTF: {ex.Message}"); ++ } ++ catch (Exception ex) ++ { ++ WriteStatus("SquidWTF API", "ERROR", ConsoleColor.Red); ++ WriteDetail(ex.Message); ++ return ValidationResult.Failure("-1", $"Validation error: {ex.Message}"); ++ } ++ } ++ ++ private async Task ValidateSearchFunctionality(CancellationToken cancellationToken) ++ { ++ try ++ { ++ // Test search with a simple query ++ var searchUrl = "https://triton.squid.wtf/search/?s=Taylor%20Swift"; ++ var searchResponse = await _httpClient.GetAsync(searchUrl, cancellationToken); ++ ++ if (searchResponse.IsSuccessStatusCode) ++ { ++ var json = await searchResponse.Content.ReadAsStringAsync(cancellationToken); ++ var doc = JsonDocument.Parse(json); ++ ++ if (doc.RootElement.TryGetProperty("data", out var data) && ++ data.TryGetProperty("items", out var items)) ++ { ++ var itemCount = items.GetArrayLength(); ++ WriteStatus("Search Functionality", "WORKING", ConsoleColor.Green); ++ WriteDetail($"Test search returned {itemCount} results"); ++ } ++ else ++ { ++ WriteStatus("Search Functionality", "UNEXPECTED RESPONSE", ConsoleColor.Yellow); ++ } ++ } ++ else ++ { ++ WriteStatus("Search Functionality", $"HTTP {(int)searchResponse.StatusCode}", ConsoleColor.Yellow); ++ } ++ } ++ catch (Exception ex) ++ { ++ WriteStatus("Search Functionality", "ERROR", ConsoleColor.Yellow); ++ WriteDetail($"Could not verify search: {ex.Message}"); ++ } ++ } ++} +\ No newline at end of file +diff --git a/allstarr/Services/StartupValidationService.cs b/allstarr/Services/StartupValidationService.cs +new file mode 100644 +index 0000000..8a522cf +--- /dev/null ++++ b/allstarr/Services/StartupValidationService.cs +@@ -0,0 +1,145 @@ ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++using allstarr.Services.Deezer; ++using allstarr.Services.Qobuz; ++ ++namespace allstarr.Services; ++ ++/// ++/// Hosted service that validates configuration at startup and logs the results. ++/// Checks connectivity to Subsonic server and validates music service credentials (Deezer or Qobuz). ++/// Uses a dedicated HttpClient without logging to keep console output clean. ++/// ++public class StartupValidationService : IHostedService ++{ ++ private readonly IConfiguration _configuration; ++ private readonly IOptions _subsonicSettings; ++ private readonly IOptions _deezerSettings; ++ private readonly IOptions _qobuzSettings; ++ private readonly HttpClient _httpClient; ++ ++ public StartupValidationService( ++ IConfiguration configuration, ++ IOptions subsonicSettings, ++ IOptions deezerSettings, ++ IOptions qobuzSettings) ++ { ++ _configuration = configuration; ++ _subsonicSettings = subsonicSettings; ++ _deezerSettings = deezerSettings; ++ _qobuzSettings = qobuzSettings; ++ // Create a dedicated HttpClient without logging to keep startup output clean ++ _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; ++ } ++ ++ public async Task StartAsync(CancellationToken cancellationToken) ++ { ++ Console.WriteLine(); ++ Console.WriteLine("========================================"); ++ Console.WriteLine(" allstarr starting up... "); ++ Console.WriteLine("========================================"); ++ Console.WriteLine(); ++ ++ await ValidateSubsonicAsync(cancellationToken); ++ ++ // Validate music service credentials based on configured service ++ var musicService = _subsonicSettings.Value.MusicService; ++ if (musicService == MusicService.Qobuz) ++ { ++ var qobuzValidator = new QobuzStartupValidator(_qobuzSettings, _httpClient); ++ await qobuzValidator.ValidateAsync(cancellationToken); ++ } ++ else ++ { ++ var deezerValidator = new DeezerStartupValidator(_deezerSettings, _httpClient); ++ await deezerValidator.ValidateAsync(cancellationToken); ++ } ++ ++ Console.WriteLine(); ++ Console.WriteLine("========================================"); ++ Console.WriteLine(" Startup validation complete "); ++ Console.WriteLine("========================================"); ++ Console.WriteLine(); ++ } ++ ++ public Task StopAsync(CancellationToken cancellationToken) ++ { ++ return Task.CompletedTask; ++ } ++ ++ private async Task ValidateSubsonicAsync(CancellationToken cancellationToken) ++ { ++ var subsonicUrl = _subsonicSettings.Value.Url; ++ ++ if (string.IsNullOrWhiteSpace(subsonicUrl)) ++ { ++ WriteStatus("Subsonic URL", "NOT CONFIGURED", ConsoleColor.Red); ++ WriteDetail("Set the Subsonic__Url environment variable"); ++ return; ++ } ++ ++ WriteStatus("Subsonic URL", subsonicUrl, ConsoleColor.Cyan); ++ ++ try ++ { ++ var pingUrl = $"{subsonicUrl.TrimEnd('/')}/rest/ping.view?v=1.16.1&c=allstarr&f=json"; ++ var response = await _httpClient.GetAsync(pingUrl, cancellationToken); ++ ++ if (response.IsSuccessStatusCode) ++ { ++ var content = await response.Content.ReadAsStringAsync(cancellationToken); ++ ++ if (content.Contains("\"status\":\"ok\"") || content.Contains("status=\"ok\"")) ++ { ++ WriteStatus("Subsonic server", "OK", ConsoleColor.Green); ++ } ++ else if (content.Contains("\"status\":\"failed\"") || content.Contains("status=\"failed\"")) ++ { ++ WriteStatus("Subsonic server", "REACHABLE", ConsoleColor.Yellow); ++ WriteDetail("Authentication may be required for some operations"); ++ } ++ else ++ { ++ WriteStatus("Subsonic server", "REACHABLE", ConsoleColor.Yellow); ++ WriteDetail("Unexpected response format"); ++ } ++ } ++ else ++ { ++ WriteStatus("Subsonic server", $"HTTP {(int)response.StatusCode}", ConsoleColor.Red); ++ } ++ } ++ catch (TaskCanceledException) ++ { ++ WriteStatus("Subsonic server", "TIMEOUT", ConsoleColor.Red); ++ WriteDetail("Could not reach server within 10 seconds"); ++ } ++ catch (HttpRequestException ex) ++ { ++ WriteStatus("Subsonic server", "UNREACHABLE", ConsoleColor.Red); ++ WriteDetail(ex.Message); ++ } ++ catch (Exception ex) ++ { ++ WriteStatus("Subsonic server", "ERROR", ConsoleColor.Red); ++ WriteDetail(ex.Message); ++ } ++ } ++ ++ private static void WriteStatus(string label, string value, ConsoleColor valueColor) ++ { ++ Console.Write($" {label}: "); ++ var originalColor = Console.ForegroundColor; ++ Console.ForegroundColor = valueColor; ++ Console.WriteLine(value); ++ Console.ForegroundColor = originalColor; ++ } ++ ++ private static void WriteDetail(string message) ++ { ++ var originalColor = Console.ForegroundColor; ++ Console.ForegroundColor = ConsoleColor.DarkGray; ++ Console.WriteLine($" -> {message}"); ++ Console.ForegroundColor = originalColor; ++ } ++} +diff --git a/allstarr/Services/Subsonic/PlaylistSyncService.cs b/allstarr/Services/Subsonic/PlaylistSyncService.cs +new file mode 100644 +index 0000000..0903213 +--- /dev/null ++++ b/allstarr/Services/Subsonic/PlaylistSyncService.cs +@@ -0,0 +1,411 @@ ++using System.Collections.Concurrent; ++using System.Text; ++using Microsoft.Extensions.Options; ++using allstarr.Models.Domain; ++using allstarr.Models.Settings; ++using allstarr.Models.Subsonic; ++using allstarr.Services.Common; ++using IOFile = System.IO.File; ++ ++namespace allstarr.Services.Subsonic; ++ ++/// ++/// Service responsible for downloading playlist tracks and creating M3U files ++/// ++public class PlaylistSyncService ++{ ++ private readonly IMusicMetadataService _deezerMetadataService; ++ private readonly IMusicMetadataService _qobuzMetadataService; ++ private readonly IEnumerable _downloadServices; ++ private readonly IConfiguration _configuration; ++ private readonly SubsonicSettings _subsonicSettings; ++ private readonly ILogger _logger; ++ ++ // In-memory cache to track which playlist a track belongs to ++ // Key: trackId (format: ext-{provider}-{externalId}), Value: playlistId ++ // TTL: 5 minutes (tracks expire automatically) ++ private readonly ConcurrentDictionary _trackPlaylistCache = new(); ++ private static readonly TimeSpan CacheTTL = TimeSpan.FromMinutes(5); ++ ++ private readonly string _musicDirectory; ++ private readonly string _playlistDirectory; ++ ++ // Cancellation token for background cleanup task ++ private readonly CancellationTokenSource _cleanupCancellationTokenSource = new(); ++ private readonly Task _cleanupTask; ++ ++ public PlaylistSyncService( ++ IEnumerable metadataServices, ++ IEnumerable downloadServices, ++ IConfiguration configuration, ++ IOptions subsonicSettings, ++ ILogger logger) ++ { ++ // Get Deezer and Qobuz metadata services ++ _deezerMetadataService = metadataServices.FirstOrDefault(s => s.GetType().Name.Contains("Deezer")) ++ ?? throw new InvalidOperationException("Deezer metadata service not found"); ++ _qobuzMetadataService = metadataServices.FirstOrDefault(s => s.GetType().Name.Contains("Qobuz")) ++ ?? throw new InvalidOperationException("Qobuz metadata service not found"); ++ ++ _downloadServices = downloadServices; ++ _configuration = configuration; ++ _subsonicSettings = subsonicSettings.Value; ++ _logger = logger; ++ ++ _musicDirectory = configuration["Library:DownloadPath"] ?? "./downloads"; ++ _playlistDirectory = Path.Combine(_musicDirectory, _subsonicSettings.PlaylistsDirectory ?? "playlists"); ++ ++ // Ensure playlists directory exists ++ if (!Directory.Exists(_playlistDirectory)) ++ { ++ Directory.CreateDirectory(_playlistDirectory); ++ } ++ ++ // Start background cleanup task for expired cache entries ++ _cleanupTask = Task.Run(() => CleanupExpiredCacheEntriesAsync(_cleanupCancellationTokenSource.Token)); ++ } ++ ++ /// ++ /// Gets the metadata service for the specified provider ++ /// ++ private IMusicMetadataService? GetMetadataServiceForProvider(string provider) ++ { ++ return provider.ToLower() switch ++ { ++ "deezer" => _deezerMetadataService, ++ "qobuz" => _qobuzMetadataService, ++ _ => null ++ }; ++ } ++ ++ /// ++ /// Adds a track to the playlist context cache. ++ /// This allows the download service to know which playlist a track belongs to. ++ /// ++ public void AddTrackToPlaylistCache(string trackId, string playlistId) ++ { ++ var expiresAt = DateTime.UtcNow.Add(CacheTTL); ++ _trackPlaylistCache[trackId] = (playlistId, expiresAt); ++ _logger.LogInformation("Added track {TrackId} to playlist cache with playlistId {PlaylistId}", trackId, playlistId); ++ } ++ ++ /// ++ /// Gets the playlist ID for a given track ID from cache. ++ /// Returns null if not found or expired. ++ /// ++ public string? GetPlaylistIdForTrack(string trackId) ++ { ++ if (_trackPlaylistCache.TryGetValue(trackId, out var entry)) ++ { ++ if (entry.ExpiresAt > DateTime.UtcNow) ++ { ++ return entry.PlaylistId; ++ } ++ ++ // Expired, remove it ++ _trackPlaylistCache.TryRemove(trackId, out _); ++ } ++ ++ return null; ++ } ++ ++ /// ++ /// Downloads all tracks from a playlist and creates an M3U file. ++ /// This is triggered when a user stars a playlist. ++ /// ++ public async Task DownloadFullPlaylistAsync(string playlistId, CancellationToken cancellationToken = default) ++ { ++ try ++ { ++ _logger.LogInformation("Starting download for playlist {PlaylistId}", playlistId); ++ ++ // Parse playlist ID ++ if (!PlaylistIdHelper.IsExternalPlaylist(playlistId)) ++ { ++ _logger.LogWarning("Invalid playlist ID format: {PlaylistId}", playlistId); ++ return; ++ } ++ ++ var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); ++ ++ // Get playlist metadata ++ var metadataService = GetMetadataServiceForProvider(provider); ++ if (metadataService == null) ++ { ++ throw new NotSupportedException($"Provider '{provider}' not supported for playlists"); ++ } ++ ++ var playlist = await metadataService.GetPlaylistAsync(provider, externalId); ++ if (playlist == null) ++ { ++ _logger.LogWarning("Playlist not found: {PlaylistId}", playlistId); ++ return; ++ } ++ ++ var tracks = await metadataService.GetPlaylistTracksAsync(provider, externalId); ++ if (tracks == null || tracks.Count == 0) ++ { ++ _logger.LogWarning("No tracks found in playlist {PlaylistId}", playlistId); ++ return; ++ } ++ ++ _logger.LogInformation("Found {TrackCount} tracks in playlist '{PlaylistName}'", tracks.Count, playlist.Name); ++ ++ // Get the appropriate download service for this provider ++ var downloadService = _downloadServices.FirstOrDefault(s => ++ s.GetType().Name.Contains(provider, StringComparison.OrdinalIgnoreCase)); ++ ++ if (downloadService == null) ++ { ++ _logger.LogError("No download service found for provider '{Provider}'", provider); ++ return; ++ } ++ ++ // Download all tracks (M3U will be created once at the end) ++ var downloadedTracks = new List<(Song Song, string LocalPath)>(); ++ ++ foreach (var track in tracks) ++ { ++ try ++ { ++ if (string.IsNullOrEmpty(track.ExternalId)) ++ { ++ _logger.LogWarning("Track has no external ID, skipping: {Title}", track.Title); ++ continue; ++ } ++ ++ // Add track to playlist cache BEFORE downloading ++ // This marks it as part of a full playlist download, so AddTrackToM3UAsync will skip real-time updates ++ var trackId = $"ext-{provider}-{track.ExternalId}"; ++ AddTrackToPlaylistCache(trackId, playlistId); ++ ++ _logger.LogInformation("Downloading track '{Artist} - {Title}'", track.Artist, track.Title); ++ var localPath = await downloadService.DownloadSongAsync(provider, track.ExternalId, cancellationToken); ++ ++ downloadedTracks.Add((track, localPath)); ++ _logger.LogDebug("Downloaded: {Path}", localPath); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Failed to download track '{Artist} - {Title}'", track.Artist, track.Title); ++ } ++ } ++ ++ if (downloadedTracks.Count == 0) ++ { ++ _logger.LogWarning("No tracks were successfully downloaded for playlist '{PlaylistName}'", playlist.Name); ++ return; ++ } ++ ++ // Create M3U file ONCE at the end with all downloaded tracks ++ await CreateM3UPlaylistAsync(playlist.Name, downloadedTracks); ++ ++ _logger.LogInformation("Playlist download completed: {DownloadedCount}/{TotalCount} tracks for '{PlaylistName}'", ++ downloadedTracks.Count, tracks.Count, playlist.Name); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to download playlist {PlaylistId}", playlistId); ++ throw; ++ } ++ } ++ ++ /// ++ /// Creates an M3U playlist file with relative paths to downloaded tracks ++ /// ++ private async Task CreateM3UPlaylistAsync(string playlistName, List<(Song Song, string LocalPath)> tracks) ++ { ++ try ++ { ++ // Sanitize playlist name for file system ++ var fileName = PathHelper.SanitizeFileName(playlistName) + ".m3u"; ++ var playlistPath = Path.Combine(_playlistDirectory, fileName); ++ ++ var m3uContent = new StringBuilder(); ++ m3uContent.AppendLine("#EXTM3U"); ++ ++ foreach (var (song, localPath) in tracks) ++ { ++ // Calculate relative path from playlist directory to track ++ var relativePath = Path.GetRelativePath(_playlistDirectory, localPath); ++ ++ // Convert backslashes to forward slashes for M3U compatibility ++ relativePath = relativePath.Replace('\\', '/'); ++ ++ // Add EXTINF line with duration and artist - title ++ var duration = song.Duration ?? 0; ++ m3uContent.AppendLine($"#EXTINF:{duration},{song.Artist} - {song.Title}"); ++ m3uContent.AppendLine(relativePath); ++ } ++ ++ await IOFile.WriteAllTextAsync(playlistPath, m3uContent.ToString()); ++ _logger.LogInformation("Created M3U playlist: {Path}", playlistPath); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to create M3U playlist for '{PlaylistName}'", playlistName); ++ throw; ++ } ++ } ++ ++ /// ++ /// Adds a track to an existing M3U playlist or creates it if it doesn't exist. ++ /// Called when individual tracks are played/downloaded (NOT during full playlist download). ++ /// The M3U is rebuilt in the correct playlist order each time. ++ /// ++ /// If true, skips M3U update (will be done at the end by DownloadFullPlaylistAsync) ++ public async Task AddTrackToM3UAsync(string playlistId, Song track, string localPath, bool isFullPlaylistDownload = false) ++ { ++ // Skip real-time updates during full playlist download (M3U will be created once at the end) ++ if (isFullPlaylistDownload) ++ { ++ _logger.LogDebug("Skipping M3U update for track {TrackId} (full playlist download in progress)", track.Id); ++ return; ++ } ++ ++ try ++ { ++ // Get playlist metadata to get the name and track order ++ if (!PlaylistIdHelper.IsExternalPlaylist(playlistId)) ++ { ++ _logger.LogWarning("Invalid playlist ID format: {PlaylistId}", playlistId); ++ return; ++ } ++ ++ var (provider, externalId) = PlaylistIdHelper.ParsePlaylistId(playlistId); ++ ++ var metadataService = GetMetadataServiceForProvider(provider); ++ if (metadataService == null) ++ { ++ _logger.LogWarning("No metadata service found for provider '{Provider}'", provider); ++ return; ++ } ++ ++ var playlist = await metadataService.GetPlaylistAsync(provider, externalId); ++ if (playlist == null) ++ { ++ _logger.LogWarning("Playlist not found: {PlaylistId}", playlistId); ++ return; ++ } ++ ++ // Get all tracks from the playlist to maintain order ++ var allPlaylistTracks = await metadataService.GetPlaylistTracksAsync(provider, externalId); ++ if (allPlaylistTracks == null || allPlaylistTracks.Count == 0) ++ { ++ _logger.LogWarning("No tracks found in playlist: {PlaylistId}", playlistId); ++ return; ++ } ++ ++ // Sanitize playlist name for file system ++ var fileName = PathHelper.SanitizeFileName(playlist.Name) + ".m3u"; ++ var playlistPath = Path.Combine(_playlistDirectory, fileName); ++ ++ // Build M3U content in the correct order ++ var m3uContent = new StringBuilder(); ++ m3uContent.AppendLine("#EXTM3U"); ++ ++ int addedCount = 0; ++ foreach (var playlistTrack in allPlaylistTracks) ++ { ++ // Check if this track has been downloaded locally ++ string? trackLocalPath = null; ++ ++ // If this is the track we just downloaded ++ if (playlistTrack.Id == track.Id) ++ { ++ trackLocalPath = localPath; ++ } ++ else ++ { ++ // Check if track was previously downloaded ++ var trackProvider = playlistTrack.ExternalProvider; ++ var trackExternalId = playlistTrack.ExternalId; ++ ++ if (!string.IsNullOrEmpty(trackProvider) && !string.IsNullOrEmpty(trackExternalId)) ++ { ++ // Try to find the download service for this provider ++ var downloadService = _downloadServices.FirstOrDefault(s => ++ s.GetType().Name.Contains(trackProvider, StringComparison.OrdinalIgnoreCase)); ++ ++ if (downloadService != null) ++ { ++ trackLocalPath = await downloadService.GetLocalPathIfExistsAsync(trackProvider, trackExternalId); ++ } ++ } ++ } ++ ++ // If track is downloaded, add it to M3U ++ if (!string.IsNullOrEmpty(trackLocalPath) && IOFile.Exists(trackLocalPath)) ++ { ++ var relativePath = Path.GetRelativePath(_playlistDirectory, trackLocalPath); ++ relativePath = relativePath.Replace('\\', '/'); ++ ++ var duration = playlistTrack.Duration ?? 0; ++ m3uContent.AppendLine($"#EXTINF:{duration},{playlistTrack.Artist} - {playlistTrack.Title}"); ++ m3uContent.AppendLine(relativePath); ++ addedCount++; ++ } ++ } ++ ++ // Write the M3U file (overwrites existing) ++ await IOFile.WriteAllTextAsync(playlistPath, m3uContent.ToString()); ++ _logger.LogInformation("Updated M3U playlist '{PlaylistName}' with {Count} tracks (in correct order)", ++ playlist.Name, addedCount); ++ } ++ catch (Exception ex) ++ { ++ _logger.LogError(ex, "Failed to add track to M3U playlist"); ++ } ++ } ++ ++ /// ++ /// Background task to clean up expired cache entries every minute ++ /// ++ private async Task CleanupExpiredCacheEntriesAsync(CancellationToken cancellationToken) ++ { ++ while (!cancellationToken.IsCancellationRequested) ++ { ++ try ++ { ++ await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); ++ ++ var now = DateTime.UtcNow; ++ var expiredKeys = _trackPlaylistCache ++ .Where(kvp => kvp.Value.ExpiresAt <= now) ++ .Select(kvp => kvp.Key) ++ .ToList(); ++ ++ foreach (var key in expiredKeys) ++ { ++ _trackPlaylistCache.TryRemove(key, out _); ++ } ++ ++ if (expiredKeys.Count > 0) ++ { ++ _logger.LogDebug("Cleaned up {Count} expired playlist cache entries", expiredKeys.Count); ++ } ++ } ++ catch (OperationCanceledException) ++ { ++ // Expected when cancellation is requested ++ break; ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Error during playlist cache cleanup"); ++ } ++ } ++ ++ _logger.LogInformation("Playlist cache cleanup task stopped"); ++ } ++ ++ /// ++ /// Stops the background cleanup task ++ /// ++ public async Task StopCleanupAsync() ++ { ++ _cleanupCancellationTokenSource.Cancel(); ++ await _cleanupTask; ++ _cleanupCancellationTokenSource.Dispose(); ++ } ++} +diff --git a/allstarr/Services/Subsonic/SubsonicModelMapper.cs b/allstarr/Services/Subsonic/SubsonicModelMapper.cs +new file mode 100644 +index 0000000..09542bc +--- /dev/null ++++ b/allstarr/Services/Subsonic/SubsonicModelMapper.cs +@@ -0,0 +1,302 @@ ++using System.Text; ++using System.Text.Json; ++using System.Xml.Linq; ++using allstarr.Models.Search; ++using allstarr.Models.Subsonic; ++ ++namespace allstarr.Services.Subsonic; ++ ++/// ++/// Handles parsing Subsonic API responses and merging local with external search results. ++/// ++public class SubsonicModelMapper ++{ ++ private readonly SubsonicResponseBuilder _responseBuilder; ++ private readonly ILogger _logger; ++ ++ public SubsonicModelMapper( ++ SubsonicResponseBuilder responseBuilder, ++ ILogger logger) ++ { ++ _responseBuilder = responseBuilder; ++ _logger = logger; ++ } ++ ++ /// ++ /// Parses a Subsonic search response and extracts songs, albums, and artists. ++ /// ++ public (List Songs, List Albums, List Artists) ParseSearchResponse( ++ byte[] responseBody, ++ string? contentType) ++ { ++ var songs = new List(); ++ var albums = new List(); ++ var artists = new List(); ++ ++ try ++ { ++ var content = Encoding.UTF8.GetString(responseBody); ++ ++ if (contentType?.Contains("json") == true) ++ { ++ var jsonDoc = JsonDocument.Parse(content); ++ if (jsonDoc.RootElement.TryGetProperty("subsonic-response", out var response) && ++ response.TryGetProperty("searchResult3", out var searchResult)) ++ { ++ if (searchResult.TryGetProperty("song", out var songElements)) ++ { ++ foreach (var song in songElements.EnumerateArray()) ++ { ++ songs.Add(_responseBuilder.ConvertSubsonicJsonElement(song, true)); ++ } ++ } ++ if (searchResult.TryGetProperty("album", out var albumElements)) ++ { ++ foreach (var album in albumElements.EnumerateArray()) ++ { ++ albums.Add(_responseBuilder.ConvertSubsonicJsonElement(album, true)); ++ } ++ } ++ if (searchResult.TryGetProperty("artist", out var artistElements)) ++ { ++ foreach (var artist in artistElements.EnumerateArray()) ++ { ++ artists.Add(_responseBuilder.ConvertSubsonicJsonElement(artist, true)); ++ } ++ } ++ } ++ } ++ else ++ { ++ var xmlDoc = XDocument.Parse(content); ++ var ns = xmlDoc.Root?.GetDefaultNamespace() ?? XNamespace.None; ++ var searchResult = xmlDoc.Descendants(ns + "searchResult3").FirstOrDefault(); ++ ++ if (searchResult != null) ++ { ++ foreach (var song in searchResult.Elements(ns + "song")) ++ { ++ songs.Add(_responseBuilder.ConvertSubsonicXmlElement(song, "song")); ++ } ++ foreach (var album in searchResult.Elements(ns + "album")) ++ { ++ albums.Add(_responseBuilder.ConvertSubsonicXmlElement(album, "album")); ++ } ++ foreach (var artist in searchResult.Elements(ns + "artist")) ++ { ++ artists.Add(_responseBuilder.ConvertSubsonicXmlElement(artist, "artist")); ++ } ++ } ++ } ++ } ++ catch (Exception ex) ++ { ++ _logger.LogWarning(ex, "Error parsing Subsonic search response"); ++ } ++ ++ return (songs, albums, artists); ++ } ++ ++ /// ++ /// Merges local and external search results (songs, albums, artists, playlists). ++ /// ++ public (List MergedSongs, List MergedAlbums, List MergedArtists) MergeSearchResults( ++ List localSongs, ++ List localAlbums, ++ List localArtists, ++ SearchResult externalResult, ++ List externalPlaylists, ++ bool isJson) ++ { ++ if (isJson) ++ { ++ return MergeSearchResultsJson(localSongs, localAlbums, localArtists, externalResult, externalPlaylists); ++ } ++ else ++ { ++ return MergeSearchResultsXml(localSongs, localAlbums, localArtists, externalResult, externalPlaylists); ++ } ++ } ++ ++ private (List MergedSongs, List MergedAlbums, List MergedArtists) MergeSearchResultsJson( ++ List localSongs, ++ List localAlbums, ++ List localArtists, ++ SearchResult externalResult, ++ List externalPlaylists) ++ { ++ var mergedSongs = localSongs ++ .Concat(externalResult.Songs.Select(s => _responseBuilder.ConvertSongToJson(s))) ++ .ToList(); ++ ++ // Merge albums with playlists (playlists appear as albums with genre "Playlist") ++ var mergedAlbums = localAlbums ++ .Concat(externalResult.Albums.Select(a => _responseBuilder.ConvertAlbumToJson(a))) ++ .Concat(externalPlaylists.Select(p => ConvertPlaylistToAlbumJson(p))) ++ .ToList(); ++ ++ // Deduplicate artists by name - prefer local artists over external ones ++ var localArtistNames = new HashSet(StringComparer.OrdinalIgnoreCase); ++ foreach (var artist in localArtists) ++ { ++ if (artist is Dictionary dict && dict.TryGetValue("name", out var nameObj)) ++ { ++ localArtistNames.Add(nameObj?.ToString() ?? ""); ++ } ++ } ++ ++ var mergedArtists = localArtists.ToList(); ++ foreach (var externalArtist in externalResult.Artists) ++ { ++ // Only add external artist if no local artist with same name exists ++ if (!localArtistNames.Contains(externalArtist.Name)) ++ { ++ mergedArtists.Add(_responseBuilder.ConvertArtistToJson(externalArtist)); ++ } ++ } ++ ++ return (mergedSongs, mergedAlbums, mergedArtists); ++ } ++ ++ private (List MergedSongs, List MergedAlbums, List MergedArtists) MergeSearchResultsXml( ++ List localSongs, ++ List localAlbums, ++ List localArtists, ++ SearchResult externalResult, ++ List externalPlaylists) ++ { ++ var ns = XNamespace.Get("http://subsonic.org/restapi"); ++ ++ // Deduplicate artists by name - prefer local artists over external ones ++ var localArtistNamesXml = new HashSet(StringComparer.OrdinalIgnoreCase); ++ var mergedArtists = new List(); ++ ++ foreach (var artist in localArtists.Cast()) ++ { ++ var name = artist.Attribute("name")?.Value; ++ if (!string.IsNullOrEmpty(name)) ++ { ++ localArtistNamesXml.Add(name); ++ } ++ artist.Name = ns + "artist"; ++ mergedArtists.Add(artist); ++ } ++ ++ foreach (var artist in externalResult.Artists) ++ { ++ // Only add external artist if no local artist with same name exists ++ if (!localArtistNamesXml.Contains(artist.Name)) ++ { ++ mergedArtists.Add(_responseBuilder.ConvertArtistToXml(artist, ns)); ++ } ++ } ++ ++ // Albums ++ var mergedAlbums = new List(); ++ foreach (var album in localAlbums.Cast()) ++ { ++ album.Name = ns + "album"; ++ mergedAlbums.Add(album); ++ } ++ foreach (var album in externalResult.Albums) ++ { ++ mergedAlbums.Add(_responseBuilder.ConvertAlbumToXml(album, ns)); ++ } ++ // Add playlists as albums ++ foreach (var playlist in externalPlaylists) ++ { ++ mergedAlbums.Add(ConvertPlaylistToAlbumXml(playlist, ns)); ++ } ++ ++ // Songs ++ var mergedSongs = new List(); ++ foreach (var song in localSongs.Cast()) ++ { ++ song.Name = ns + "song"; ++ mergedSongs.Add(song); ++ } ++ foreach (var song in externalResult.Songs) ++ { ++ mergedSongs.Add(_responseBuilder.ConvertSongToXml(song, ns)); ++ } ++ ++ return (mergedSongs, mergedAlbums, mergedArtists); ++ } ++ ++ /// ++ /// Converts an ExternalPlaylist to a JSON object representing an album. ++ /// Playlists are represented as albums with genre "Playlist" and artist "🎵 {Provider} {Curator}". ++ /// ++ private Dictionary ConvertPlaylistToAlbumJson(ExternalPlaylist playlist) ++ { ++ var artistName = $"🎵 {char.ToUpper(playlist.Provider[0])}{playlist.Provider.Substring(1)}"; ++ if (!string.IsNullOrEmpty(playlist.CuratorName)) ++ { ++ artistName += $" {playlist.CuratorName}"; ++ } ++ ++ var artistId = $"curator-{playlist.Provider}-{playlist.CuratorName?.ToLowerInvariant().Replace(" ", "-") ?? "unknown"}"; ++ ++ var album = new Dictionary ++ { ++ ["id"] = playlist.Id, ++ ["name"] = playlist.Name, ++ ["artist"] = artistName, ++ ["artistId"] = artistId, ++ ["genre"] = "Playlist", ++ ["songCount"] = playlist.TrackCount, ++ ["duration"] = playlist.Duration ++ }; ++ ++ if (playlist.CreatedDate.HasValue) ++ { ++ album["year"] = playlist.CreatedDate.Value.Year; ++ album["created"] = playlist.CreatedDate.Value.ToString("yyyy-MM-ddTHH:mm:ss"); ++ } ++ ++ if (!string.IsNullOrEmpty(playlist.CoverUrl)) ++ { ++ album["coverArt"] = playlist.Id; ++ } ++ ++ return album; ++ } ++ ++ /// ++ /// Converts an ExternalPlaylist to an XML element representing an album. ++ /// Playlists are represented as albums with genre "Playlist" and artist "🎵 {Provider} {Curator}". ++ /// ++ private XElement ConvertPlaylistToAlbumXml(ExternalPlaylist playlist, XNamespace ns) ++ { ++ var artistName = $"🎵 {char.ToUpper(playlist.Provider[0])}{playlist.Provider.Substring(1)}"; ++ if (!string.IsNullOrEmpty(playlist.CuratorName)) ++ { ++ artistName += $" {playlist.CuratorName}"; ++ } ++ ++ var artistId = $"curator-{playlist.Provider}-{playlist.CuratorName?.ToLowerInvariant().Replace(" ", "-") ?? "unknown"}"; ++ ++ var album = new XElement(ns + "album", ++ new XAttribute("id", playlist.Id), ++ new XAttribute("name", playlist.Name), ++ new XAttribute("artist", artistName), ++ new XAttribute("artistId", artistId), ++ new XAttribute("genre", "Playlist"), ++ new XAttribute("songCount", playlist.TrackCount), ++ new XAttribute("duration", playlist.Duration) ++ ); ++ ++ if (playlist.CreatedDate.HasValue) ++ { ++ album.Add(new XAttribute("year", playlist.CreatedDate.Value.Year)); ++ album.Add(new XAttribute("created", playlist.CreatedDate.Value.ToString("yyyy-MM-ddTHH:mm:ss"))); ++ } ++ ++ if (!string.IsNullOrEmpty(playlist.CoverUrl)) ++ { ++ album.Add(new XAttribute("coverArt", playlist.Id)); ++ } ++ ++ return album; ++ } ++} +diff --git a/allstarr/Services/Subsonic/SubsonicProxyService.cs b/allstarr/Services/Subsonic/SubsonicProxyService.cs +new file mode 100644 +index 0000000..34a9a31 +--- /dev/null ++++ b/allstarr/Services/Subsonic/SubsonicProxyService.cs +@@ -0,0 +1,150 @@ ++using Microsoft.AspNetCore.Mvc; ++using allstarr.Models.Settings; ++ ++namespace allstarr.Services.Subsonic; ++ ++/// ++/// Handles proxying requests to the underlying Subsonic server. ++/// ++public class SubsonicProxyService ++{ ++ private readonly HttpClient _httpClient; ++ private readonly SubsonicSettings _subsonicSettings; ++ private readonly IHttpContextAccessor _httpContextAccessor; ++ ++ public SubsonicProxyService( ++ IHttpClientFactory httpClientFactory, ++ Microsoft.Extensions.Options.IOptions subsonicSettings, ++ IHttpContextAccessor httpContextAccessor) ++ { ++ _httpClient = httpClientFactory.CreateClient(); ++ _subsonicSettings = subsonicSettings.Value; ++ _httpContextAccessor = httpContextAccessor; ++ } ++ ++ /// ++ /// Relays a request to the Subsonic server and returns the response. ++ /// ++ public async Task<(byte[] Body, string? ContentType)> RelayAsync( ++ string endpoint, ++ Dictionary parameters) ++ { ++ var query = string.Join("&", parameters.Select(kv => ++ $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); ++ var url = $"{_subsonicSettings.Url}/{endpoint}?{query}"; ++ ++ HttpResponseMessage response = await _httpClient.GetAsync(url); ++ response.EnsureSuccessStatusCode(); ++ ++ var body = await response.Content.ReadAsByteArrayAsync(); ++ var contentType = response.Content.Headers.ContentType?.ToString(); ++ ++ return (body, contentType); ++ } ++ ++ /// ++ /// Safely relays a request to the Subsonic server, returning null on failure. ++ /// ++ public async Task<(byte[]? Body, string? ContentType, bool Success)> RelaySafeAsync( ++ string endpoint, ++ Dictionary parameters) ++ { ++ try ++ { ++ var result = await RelayAsync(endpoint, parameters); ++ return (result.Body, result.ContentType, true); ++ } ++ catch ++ { ++ return (null, null, false); ++ } ++ } ++ ++ private static readonly string[] StreamingRequiredHeaders = ++ { ++ "Accept-Ranges", ++ "Content-Range", ++ "Content-Length", ++ "ETag", ++ "Last-Modified" ++ }; ++ ++ /// ++ /// Relays a stream request to the Subsonic server with range processing support. ++ /// ++ public async Task RelayStreamAsync( ++ Dictionary parameters, ++ CancellationToken cancellationToken) ++ { ++ try ++ { ++ // Get HTTP context for request/response forwarding ++ var httpContext = _httpContextAccessor.HttpContext; ++ if (httpContext == null) ++ { ++ return new ObjectResult(new { error = "HTTP context not available" }) ++ { ++ StatusCode = 500 ++ }; ++ } ++ ++ var incomingRequest = httpContext.Request; ++ var outgoingResponse = httpContext.Response; ++ ++ var query = string.Join("&", parameters.Select(kv => ++ $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); ++ var url = $"{_subsonicSettings.Url}/rest/stream?{query}"; ++ ++ using var request = new HttpRequestMessage(HttpMethod.Get, url); ++ ++ // Forward Range headers for progressive streaming support (iOS clients) ++ if (incomingRequest.Headers.TryGetValue("Range", out var range)) ++ { ++ request.Headers.TryAddWithoutValidation("Range", range.ToArray()); ++ } ++ ++ if (incomingRequest.Headers.TryGetValue("If-Range", out var ifRange)) ++ { ++ request.Headers.TryAddWithoutValidation("If-Range", ifRange.ToArray()); ++ } ++ ++ var response = await _httpClient.SendAsync( ++ request, ++ HttpCompletionOption.ResponseHeadersRead, ++ cancellationToken); ++ ++ if (!response.IsSuccessStatusCode) ++ { ++ return new StatusCodeResult((int)response.StatusCode); ++ } ++ ++ // Forward HTTP status code (e.g., 206 Partial Content for range requests) ++ outgoingResponse.StatusCode = (int)response.StatusCode; ++ ++ // Forward streaming-required headers from upstream response ++ foreach (var header in StreamingRequiredHeaders) ++ { ++ if (response.Headers.TryGetValues(header, out var values) || ++ response.Content.Headers.TryGetValues(header, out values)) ++ { ++ outgoingResponse.Headers[header] = values.ToArray(); ++ } ++ } ++ ++ var stream = await response.Content.ReadAsStreamAsync(cancellationToken); ++ var contentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg"; ++ ++ return new FileStreamResult(stream, contentType) ++ { ++ EnableRangeProcessing = true ++ }; ++ } ++ catch (Exception ex) ++ { ++ return new ObjectResult(new { error = $"Error streaming from Subsonic: {ex.Message}" }) ++ { ++ StatusCode = 500 ++ }; ++ } ++ } ++} +diff --git a/allstarr/Services/Subsonic/SubsonicRequestParser.cs b/allstarr/Services/Subsonic/SubsonicRequestParser.cs +new file mode 100644 +index 0000000..a55d8b1 +--- /dev/null ++++ b/allstarr/Services/Subsonic/SubsonicRequestParser.cs +@@ -0,0 +1,105 @@ ++using Microsoft.AspNetCore.WebUtilities; ++using System.Text.Json; ++ ++namespace allstarr.Services.Subsonic; ++ ++/// ++/// Service responsible for parsing HTTP request parameters from various sources ++/// (query string, form body, JSON body) for Subsonic API requests. ++/// ++public class SubsonicRequestParser ++{ ++ /// ++ /// Extracts all parameters from an HTTP request (query parameters + body parameters). ++ /// Supports multiple content types: application/x-www-form-urlencoded and application/json. ++ /// ++ /// The HTTP request to parse ++ /// Dictionary containing all extracted parameters ++ public async Task> ExtractAllParametersAsync(HttpRequest request) ++ { ++ var parameters = new Dictionary(); ++ ++ // Get query parameters ++ foreach (var query in request.Query) ++ { ++ parameters[query.Key] = query.Value.ToString(); ++ } ++ ++ // Get body parameters ++ if (request.ContentLength > 0 || request.ContentType != null) ++ { ++ // Handle application/x-www-form-urlencoded (OpenSubsonic formPost extension) ++ if (request.HasFormContentType) ++ { ++ await ExtractFormParametersAsync(request, parameters); ++ } ++ // Handle application/json ++ else if (request.ContentType?.Contains("application/json") == true) ++ { ++ await ExtractJsonParametersAsync(request, parameters); ++ } ++ } ++ ++ return parameters; ++ } ++ ++ /// ++ /// Extracts parameters from form-encoded request body. ++ /// ++ private async Task ExtractFormParametersAsync(HttpRequest request, Dictionary parameters) ++ { ++ try ++ { ++ var form = await request.ReadFormAsync(); ++ foreach (var field in form) ++ { ++ parameters[field.Key] = field.Value.ToString(); ++ } ++ } ++ catch ++ { ++ // Fall back to manual parsing if ReadFormAsync fails ++ request.EnableBuffering(); ++ using var reader = new StreamReader(request.Body, leaveOpen: true); ++ var body = await reader.ReadToEndAsync(); ++ request.Body.Position = 0; ++ ++ if (!string.IsNullOrEmpty(body)) ++ { ++ var formParams = QueryHelpers.ParseQuery(body); ++ foreach (var param in formParams) ++ { ++ parameters[param.Key] = param.Value.ToString(); ++ } ++ } ++ } ++ } ++ ++ /// ++ /// Extracts parameters from JSON request body. ++ /// ++ private async Task ExtractJsonParametersAsync(HttpRequest request, Dictionary parameters) ++ { ++ using var reader = new StreamReader(request.Body); ++ var body = await reader.ReadToEndAsync(); ++ ++ if (!string.IsNullOrEmpty(body)) ++ { ++ try ++ { ++ var bodyParams = JsonSerializer.Deserialize>(body); ++ if (bodyParams != null) ++ { ++ foreach (var param in bodyParams) ++ { ++ parameters[param.Key] = param.Value?.ToString() ?? ""; ++ } ++ } ++ } ++ catch (JsonException) ++ { ++ // Ignore JSON parsing errors ++ } ++ } ++ } ++} +diff --git a/allstarr/Services/Subsonic/SubsonicResponseBuilder.cs b/allstarr/Services/Subsonic/SubsonicResponseBuilder.cs +new file mode 100644 +index 0000000..26cadcc +--- /dev/null ++++ b/allstarr/Services/Subsonic/SubsonicResponseBuilder.cs +@@ -0,0 +1,419 @@ ++using Microsoft.AspNetCore.Mvc; ++using System.Xml.Linq; ++using System.Text.Json; ++using allstarr.Models.Domain; ++using allstarr.Models.Subsonic; ++ ++namespace allstarr.Services.Subsonic; ++ ++/// ++/// Handles building Subsonic API responses in both XML and JSON formats. ++/// ++public class SubsonicResponseBuilder ++{ ++ private const string SubsonicNamespace = "http://subsonic.org/restapi"; ++ private const string SubsonicVersion = "1.16.1"; ++ ++ /// ++ /// Creates a generic Subsonic response with status "ok". ++ /// ++ public IActionResult CreateResponse(string format, string elementName, object data) ++ { ++ if (format == "json") ++ { ++ return CreateJsonResponse(new { status = "ok", version = SubsonicVersion }); ++ } ++ ++ var ns = XNamespace.Get(SubsonicNamespace); ++ var doc = new XDocument( ++ new XElement(ns + "subsonic-response", ++ new XAttribute("status", "ok"), ++ new XAttribute("version", SubsonicVersion), ++ new XElement(ns + elementName) ++ ) ++ ); ++ return new ContentResult { Content = doc.ToString(), ContentType = "application/xml" }; ++ } ++ ++ /// ++ /// Creates a Subsonic error response. ++ /// ++ public IActionResult CreateError(string format, int code, string message) ++ { ++ if (format == "json") ++ { ++ return CreateJsonResponse(new ++ { ++ status = "failed", ++ version = SubsonicVersion, ++ error = new { code, message } ++ }); ++ } ++ ++ var ns = XNamespace.Get(SubsonicNamespace); ++ var doc = new XDocument( ++ new XElement(ns + "subsonic-response", ++ new XAttribute("status", "failed"), ++ new XAttribute("version", SubsonicVersion), ++ new XElement(ns + "error", ++ new XAttribute("code", code), ++ new XAttribute("message", message) ++ ) ++ ) ++ ); ++ return new ContentResult { Content = doc.ToString(), ContentType = "application/xml" }; ++ } ++ ++ /// ++ /// Creates a Subsonic response containing a single song. ++ /// ++ public IActionResult CreateSongResponse(string format, Song song) ++ { ++ if (format == "json") ++ { ++ return CreateJsonResponse(new ++ { ++ status = "ok", ++ version = SubsonicVersion, ++ song = ConvertSongToJson(song) ++ }); ++ } ++ ++ var ns = XNamespace.Get(SubsonicNamespace); ++ var doc = new XDocument( ++ new XElement(ns + "subsonic-response", ++ new XAttribute("status", "ok"), ++ new XAttribute("version", SubsonicVersion), ++ ConvertSongToXml(song, ns) ++ ) ++ ); ++ return new ContentResult { Content = doc.ToString(), ContentType = "application/xml" }; ++ } ++ ++ /// ++ /// Creates a Subsonic response containing an album with songs. ++ /// ++ public IActionResult CreateAlbumResponse(string format, Album album) ++ { ++ var totalDuration = album.Songs.Sum(s => s.Duration ?? 0); ++ ++ if (format == "json") ++ { ++ return CreateJsonResponse(new ++ { ++ status = "ok", ++ version = SubsonicVersion, ++ album = new ++ { ++ id = album.Id, ++ name = album.Title, ++ artist = album.Artist, ++ artistId = album.ArtistId, ++ coverArt = album.Id, ++ songCount = album.Songs.Count > 0 ? album.Songs.Count : (album.SongCount ?? 0), ++ duration = totalDuration, ++ year = album.Year ?? 0, ++ genre = album.Genre ?? "", ++ isCompilation = false, ++ song = album.Songs.Select(s => ConvertSongToJson(s)).ToList() ++ } ++ }); ++ } ++ ++ var ns = XNamespace.Get(SubsonicNamespace); ++ var doc = new XDocument( ++ new XElement(ns + "subsonic-response", ++ new XAttribute("status", "ok"), ++ new XAttribute("version", SubsonicVersion), ++ new XElement(ns + "album", ++ new XAttribute("id", album.Id), ++ new XAttribute("name", album.Title), ++ new XAttribute("artist", album.Artist ?? ""), ++ new XAttribute("songCount", album.SongCount ?? 0), ++ new XAttribute("year", album.Year ?? 0), ++ new XAttribute("coverArt", album.Id), ++ album.Songs.Select(s => ConvertSongToXml(s, ns)) ++ ) ++ ) ++ ); ++ return new ContentResult { Content = doc.ToString(), ContentType = "application/xml" }; ++ } ++ ++ /// ++ /// Creates a Subsonic response for a playlist represented as an album. ++ /// Playlists appear as albums with genre "Playlist". ++ /// ++ public IActionResult CreatePlaylistAsAlbumResponse(string format, ExternalPlaylist playlist, List tracks) ++ { ++ var totalDuration = tracks.Sum(s => s.Duration ?? 0); ++ ++ // Build artist name with emoji and curator ++ var artistName = $"🎵 {char.ToUpper(playlist.Provider[0])}{playlist.Provider.Substring(1)}"; ++ if (!string.IsNullOrEmpty(playlist.CuratorName)) ++ { ++ artistName += $" {playlist.CuratorName}"; ++ } ++ ++ var artistId = $"curator-{playlist.Provider}-{playlist.CuratorName?.ToLowerInvariant().Replace(" ", "-") ?? "unknown"}"; ++ ++ if (format == "json") ++ { ++ return CreateJsonResponse(new ++ { ++ status = "ok", ++ version = SubsonicVersion, ++ album = new ++ { ++ id = playlist.Id, ++ name = playlist.Name, ++ artist = artistName, ++ artistId = artistId, ++ coverArt = playlist.Id, ++ songCount = tracks.Count, ++ duration = totalDuration, ++ year = playlist.CreatedDate?.Year ?? 0, ++ genre = "Playlist", ++ isCompilation = false, ++ created = playlist.CreatedDate?.ToString("yyyy-MM-ddTHH:mm:ss"), ++ song = tracks.Select(s => ConvertSongToJson(s)).ToList() ++ } ++ }); ++ } ++ ++ var ns = XNamespace.Get(SubsonicNamespace); ++ var albumElement = new XElement(ns + "album", ++ new XAttribute("id", playlist.Id), ++ new XAttribute("name", playlist.Name), ++ new XAttribute("artist", artistName), ++ new XAttribute("artistId", artistId), ++ new XAttribute("songCount", tracks.Count), ++ new XAttribute("duration", totalDuration), ++ new XAttribute("genre", "Playlist"), ++ new XAttribute("coverArt", playlist.Id) ++ ); ++ ++ if (playlist.CreatedDate.HasValue) ++ { ++ albumElement.Add(new XAttribute("year", playlist.CreatedDate.Value.Year)); ++ albumElement.Add(new XAttribute("created", playlist.CreatedDate.Value.ToString("yyyy-MM-ddTHH:mm:ss"))); ++ } ++ ++ // Add songs ++ foreach (var song in tracks) ++ { ++ albumElement.Add(ConvertSongToXml(song, ns)); ++ } ++ ++ var doc = new XDocument( ++ new XElement(ns + "subsonic-response", ++ new XAttribute("status", "ok"), ++ new XAttribute("version", SubsonicVersion), ++ albumElement ++ ) ++ ); ++ return new ContentResult { Content = doc.ToString(), ContentType = "application/xml" }; ++ } ++ ++ /// ++ /// Creates a Subsonic response containing an artist with albums. ++ /// ++ public IActionResult CreateArtistResponse(string format, Artist artist, List albums) ++ { ++ if (format == "json") ++ { ++ return CreateJsonResponse(new ++ { ++ status = "ok", ++ version = SubsonicVersion, ++ artist = new ++ { ++ id = artist.Id, ++ name = artist.Name, ++ coverArt = artist.Id, ++ albumCount = albums.Count, ++ artistImageUrl = artist.ImageUrl, ++ album = albums.Select(a => ConvertAlbumToJson(a)).ToList() ++ } ++ }); ++ } ++ ++ var ns = XNamespace.Get(SubsonicNamespace); ++ var doc = new XDocument( ++ new XElement(ns + "subsonic-response", ++ new XAttribute("status", "ok"), ++ new XAttribute("version", SubsonicVersion), ++ new XElement(ns + "artist", ++ new XAttribute("id", artist.Id), ++ new XAttribute("name", artist.Name), ++ new XAttribute("coverArt", artist.Id), ++ new XAttribute("albumCount", albums.Count), ++ albums.Select(a => ConvertAlbumToXml(a, ns)) ++ ) ++ ) ++ ); ++ return new ContentResult { Content = doc.ToString(), ContentType = "application/xml" }; ++ } ++ ++ /// ++ /// Creates a JSON Subsonic response with "subsonic-response" key (with hyphen). ++ /// ++ public IActionResult CreateJsonResponse(object responseContent) ++ { ++ var response = new Dictionary ++ { ++ ["subsonic-response"] = responseContent ++ }; ++ return new JsonResult(response); ++ } ++ ++ /// ++ /// Converts a Song domain model to Subsonic JSON format. ++ /// ++ public Dictionary ConvertSongToJson(Song song) ++ { ++ var result = new Dictionary ++ { ++ ["id"] = song.Id, ++ ["parent"] = song.AlbumId ?? "", ++ ["isDir"] = false, ++ ["title"] = song.Title, ++ ["album"] = song.Album ?? "", ++ ["artist"] = song.Artist ?? "", ++ ["albumId"] = song.AlbumId ?? "", ++ ["artistId"] = song.ArtistId ?? "", ++ ["duration"] = song.Duration ?? 0, ++ ["track"] = song.Track ?? 0, ++ ["year"] = song.Year ?? 0, ++ ["coverArt"] = song.Id, ++ ["suffix"] = song.IsLocal ? "mp3" : "Remote", ++ ["contentType"] = "audio/mpeg", ++ ["type"] = "music", ++ ["isVideo"] = false, ++ ["isExternal"] = !song.IsLocal ++ }; ++ ++ result["bitRate"] = song.IsLocal ? 128 : 0; // Default bitrate for local files ++ ++ return result; ++ } ++ ++ /// ++ /// Converts an Album domain model to Subsonic JSON format. ++ /// ++ public object ConvertAlbumToJson(Album album) ++ { ++ return new ++ { ++ id = album.Id, ++ name = album.Title, ++ artist = album.Artist, ++ artistId = album.ArtistId, ++ songCount = album.SongCount ?? 0, ++ year = album.Year ?? 0, ++ coverArt = album.Id, ++ isExternal = !album.IsLocal ++ }; ++ } ++ ++ /// ++ /// Converts an Artist domain model to Subsonic JSON format. ++ /// ++ public object ConvertArtistToJson(Artist artist) ++ { ++ return new ++ { ++ id = artist.Id, ++ name = artist.Name, ++ albumCount = artist.AlbumCount ?? 0, ++ coverArt = artist.Id, ++ isExternal = !artist.IsLocal ++ }; ++ } ++ ++ /// ++ /// Converts a Song domain model to Subsonic XML format. ++ /// ++ public XElement ConvertSongToXml(Song song, XNamespace ns) ++ { ++ return new XElement(ns + "song", ++ new XAttribute("id", song.Id), ++ new XAttribute("title", song.Title), ++ new XAttribute("album", song.Album ?? ""), ++ new XAttribute("artist", song.Artist ?? ""), ++ new XAttribute("duration", song.Duration ?? 0), ++ new XAttribute("track", song.Track ?? 0), ++ new XAttribute("year", song.Year ?? 0), ++ new XAttribute("coverArt", song.Id), ++ new XAttribute("isExternal", (!song.IsLocal).ToString().ToLower()) ++ ); ++ } ++ ++ /// ++ /// Converts an Album domain model to Subsonic XML format. ++ /// ++ public XElement ConvertAlbumToXml(Album album, XNamespace ns) ++ { ++ return new XElement(ns + "album", ++ new XAttribute("id", album.Id), ++ new XAttribute("name", album.Title), ++ new XAttribute("artist", album.Artist ?? ""), ++ new XAttribute("songCount", album.SongCount ?? 0), ++ new XAttribute("year", album.Year ?? 0), ++ new XAttribute("coverArt", album.Id), ++ new XAttribute("isExternal", (!album.IsLocal).ToString().ToLower()) ++ ); ++ } ++ ++ /// ++ /// Converts an Artist domain model to Subsonic XML format. ++ /// ++ public XElement ConvertArtistToXml(Artist artist, XNamespace ns) ++ { ++ return new XElement(ns + "artist", ++ new XAttribute("id", artist.Id), ++ new XAttribute("name", artist.Name), ++ new XAttribute("albumCount", artist.AlbumCount ?? 0), ++ new XAttribute("coverArt", artist.Id), ++ new XAttribute("isExternal", (!artist.IsLocal).ToString().ToLower()) ++ ); ++ } ++ ++ /// ++ /// Converts a Subsonic JSON element to a dictionary. ++ /// ++ public object ConvertSubsonicJsonElement(JsonElement element, bool isLocal) ++ { ++ var dict = new Dictionary(); ++ foreach (var prop in element.EnumerateObject()) ++ { ++ dict[prop.Name] = ConvertJsonValue(prop.Value); ++ } ++ dict["isExternal"] = !isLocal; ++ return dict; ++ } ++ ++ /// ++ /// Converts a Subsonic XML element. ++ /// ++ public XElement ConvertSubsonicXmlElement(XElement element, string type) ++ { ++ var newElement = new XElement(element); ++ newElement.SetAttributeValue("isExternal", "false"); ++ return newElement; ++ } ++ ++ private object ConvertJsonValue(JsonElement value) ++ { ++ return value.ValueKind switch ++ { ++ JsonValueKind.String => value.GetString() ?? "", ++ JsonValueKind.Number => value.TryGetInt32(out var i) ? i : value.GetDouble(), ++ JsonValueKind.True => true, ++ JsonValueKind.False => false, ++ JsonValueKind.Array => value.EnumerateArray().Select(ConvertJsonValue).ToList(), ++ JsonValueKind.Object => value.EnumerateObject().ToDictionary(p => p.Name, p => ConvertJsonValue(p.Value)), ++ JsonValueKind.Null => null!, ++ _ => value.ToString() ++ }; ++ } ++} +diff --git a/allstarr/Services/Validation/BaseStartupValidator.cs b/allstarr/Services/Validation/BaseStartupValidator.cs +new file mode 100644 +index 0000000..a0bcbd8 +--- /dev/null ++++ b/allstarr/Services/Validation/BaseStartupValidator.cs +@@ -0,0 +1,95 @@ ++namespace allstarr.Services.Validation; ++ ++/// ++/// Base class for startup validators providing common functionality ++/// ++public abstract class BaseStartupValidator : IStartupValidator ++{ ++ protected readonly HttpClient _httpClient; ++ ++ protected BaseStartupValidator(HttpClient httpClient) ++ { ++ _httpClient = httpClient; ++ } ++ ++ /// ++ /// Gets the name of the service being validated ++ /// ++ public abstract string ServiceName { get; } ++ ++ /// ++ /// Validates the service configuration and connectivity ++ /// ++ public abstract Task ValidateAsync(CancellationToken cancellationToken); ++ ++ /// ++ /// Writes a status line to the console with colored output ++ /// ++ protected static void WriteStatus(string label, string value, ConsoleColor valueColor) ++ { ++ Console.Write($" {label}: "); ++ var originalColor = Console.ForegroundColor; ++ Console.ForegroundColor = valueColor; ++ Console.WriteLine(value); ++ Console.ForegroundColor = originalColor; ++ } ++ ++ /// ++ /// Writes a detail line to the console in dark gray ++ /// ++ protected static void WriteDetail(string message) ++ { ++ var originalColor = Console.ForegroundColor; ++ Console.ForegroundColor = ConsoleColor.DarkGray; ++ Console.WriteLine($" -> {message}"); ++ Console.ForegroundColor = originalColor; ++ } ++ ++ /// ++ /// Masks a secret string for display, showing only the first few characters ++ /// ++ protected static string MaskSecret(string secret) ++ { ++ if (string.IsNullOrEmpty(secret)) ++ { ++ return "(empty)"; ++ } ++ ++ const int visibleChars = 4; ++ if (secret.Length <= visibleChars) ++ { ++ return new string('*', secret.Length); ++ } ++ ++ return secret[..visibleChars] + new string('*', Math.Min(secret.Length - visibleChars, 8)); ++ } ++ ++ /// ++ /// Handles common HTTP exceptions and returns appropriate validation result ++ /// ++ protected static ValidationResult HandleException(Exception ex, string fieldName) ++ { ++ return ex switch ++ { ++ TaskCanceledException => ValidationResult.Failure("TIMEOUT", ++ "Could not reach service within timeout period", ConsoleColor.Yellow), ++ ++ HttpRequestException httpEx => ValidationResult.Failure("UNREACHABLE", ++ httpEx.Message, ConsoleColor.Yellow), ++ ++ _ => ValidationResult.Failure("ERROR", ex.Message, ConsoleColor.Red) ++ }; ++ } ++ ++ /// ++ /// Writes validation result to console ++ /// ++ protected void WriteValidationResult(string fieldName, ValidationResult result) ++ { ++ WriteStatus(fieldName, result.Status, result.StatusColor); ++ if (!string.IsNullOrEmpty(result.Details)) ++ { ++ WriteDetail(result.Details); ++ } ++ } ++} +diff --git a/allstarr/Services/Validation/IStartupValidator.cs b/allstarr/Services/Validation/IStartupValidator.cs +new file mode 100644 +index 0000000..c23464c +--- /dev/null ++++ b/allstarr/Services/Validation/IStartupValidator.cs +@@ -0,0 +1,19 @@ ++namespace allstarr.Services.Validation; ++ ++/// ++/// Interface for service startup validators ++/// ++public interface IStartupValidator ++{ ++ /// ++ /// Gets the name of the service being validated ++ /// ++ string ServiceName { get; } ++ ++ /// ++ /// Validates the service configuration and connectivity ++ /// ++ /// Cancellation token ++ /// Validation result containing status and details ++ Task ValidateAsync(CancellationToken cancellationToken); ++} +diff --git a/allstarr/Services/Validation/StartupValidationOrchestrator.cs b/allstarr/Services/Validation/StartupValidationOrchestrator.cs +new file mode 100644 +index 0000000..dca9420 +--- /dev/null ++++ b/allstarr/Services/Validation/StartupValidationOrchestrator.cs +@@ -0,0 +1,55 @@ ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++ ++namespace allstarr.Services.Validation; ++ ++/// ++/// Orchestrates startup validation for all configured services. ++/// This replaces the old StartupValidationService with a more extensible architecture. ++/// ++public class StartupValidationOrchestrator : IHostedService ++{ ++ private readonly IEnumerable _validators; ++ private readonly IOptions _subsonicSettings; ++ ++ public StartupValidationOrchestrator( ++ IEnumerable validators, ++ IOptions subsonicSettings) ++ { ++ _validators = validators; ++ _subsonicSettings = subsonicSettings; ++ } ++ ++ public async Task StartAsync(CancellationToken cancellationToken) ++ { ++ Console.WriteLine(); ++ Console.WriteLine("========================================"); ++ Console.WriteLine(" allstarr starting up... "); ++ Console.WriteLine("========================================"); ++ Console.WriteLine(); ++ ++ // Run all validators ++ foreach (var validator in _validators) ++ { ++ try ++ { ++ await validator.ValidateAsync(cancellationToken); ++ } ++ catch (Exception ex) ++ { ++ Console.WriteLine($"Error validating {validator.ServiceName}: {ex.Message}"); ++ } ++ } ++ ++ Console.WriteLine(); ++ Console.WriteLine("========================================"); ++ Console.WriteLine(" Startup validation complete "); ++ Console.WriteLine("========================================"); ++ Console.WriteLine(); ++ } ++ ++ public Task StopAsync(CancellationToken cancellationToken) ++ { ++ return Task.CompletedTask; ++ } ++} +diff --git a/allstarr/Services/Validation/SubsonicStartupValidator.cs b/allstarr/Services/Validation/SubsonicStartupValidator.cs +new file mode 100644 +index 0000000..6691fe5 +--- /dev/null ++++ b/allstarr/Services/Validation/SubsonicStartupValidator.cs +@@ -0,0 +1,87 @@ ++using Microsoft.Extensions.Options; ++using allstarr.Models.Settings; ++ ++namespace allstarr.Services.Validation; ++ ++/// ++/// Validates Subsonic server connectivity at startup ++/// ++public class SubsonicStartupValidator : BaseStartupValidator ++{ ++ private readonly IOptions _subsonicSettings; ++ ++ public override string ServiceName => "Subsonic"; ++ ++ public SubsonicStartupValidator(IOptions subsonicSettings, HttpClient httpClient) ++ : base(httpClient) ++ { ++ _subsonicSettings = subsonicSettings; ++ } ++ ++ public override async Task ValidateAsync(CancellationToken cancellationToken) ++ { ++ var subsonicUrl = _subsonicSettings.Value.Url; ++ ++ if (string.IsNullOrWhiteSpace(subsonicUrl)) ++ { ++ WriteStatus("Subsonic URL", "NOT CONFIGURED", ConsoleColor.Red); ++ WriteDetail("Set the Subsonic__Url environment variable"); ++ return ValidationResult.NotConfigured("Subsonic URL not configured"); ++ } ++ ++ WriteStatus("Subsonic URL", subsonicUrl, ConsoleColor.Cyan); ++ ++ try ++ { ++ var pingUrl = $"{subsonicUrl.TrimEnd('/')}/rest/ping.view?v=1.16.1&c=allstarr&f=json"; ++ var response = await _httpClient.GetAsync(pingUrl, cancellationToken); ++ ++ if (response.IsSuccessStatusCode) ++ { ++ var content = await response.Content.ReadAsStringAsync(cancellationToken); ++ ++ if (content.Contains("\"status\":\"ok\"") || content.Contains("status=\"ok\"")) ++ { ++ WriteStatus("Subsonic server", "OK", ConsoleColor.Green); ++ return ValidationResult.Success("Subsonic server is accessible"); ++ } ++ else if (content.Contains("\"status\":\"failed\"") || content.Contains("status=\"failed\"")) ++ { ++ WriteStatus("Subsonic server", "REACHABLE", ConsoleColor.Yellow); ++ WriteDetail("Authentication may be required for some operations"); ++ return ValidationResult.Success("Subsonic server is reachable"); ++ } ++ else ++ { ++ WriteStatus("Subsonic server", "REACHABLE", ConsoleColor.Yellow); ++ WriteDetail("Unexpected response format"); ++ return ValidationResult.Success("Subsonic server is reachable"); ++ } ++ } ++ else ++ { ++ WriteStatus("Subsonic server", $"HTTP {(int)response.StatusCode}", ConsoleColor.Red); ++ return ValidationResult.Failure($"HTTP {(int)response.StatusCode}", ++ "Subsonic server returned an error", ConsoleColor.Red); ++ } ++ } ++ catch (TaskCanceledException) ++ { ++ WriteStatus("Subsonic server", "TIMEOUT", ConsoleColor.Red); ++ WriteDetail("Could not reach server within 10 seconds"); ++ return ValidationResult.Failure("TIMEOUT", "Could not reach server within timeout period", ConsoleColor.Red); ++ } ++ catch (HttpRequestException ex) ++ { ++ WriteStatus("Subsonic server", "UNREACHABLE", ConsoleColor.Red); ++ WriteDetail(ex.Message); ++ return ValidationResult.Failure("UNREACHABLE", ex.Message, ConsoleColor.Red); ++ } ++ catch (Exception ex) ++ { ++ WriteStatus("Subsonic server", "ERROR", ConsoleColor.Red); ++ WriteDetail(ex.Message); ++ return ValidationResult.Failure("ERROR", ex.Message, ConsoleColor.Red); ++ } ++ } ++} +diff --git a/allstarr/Services/Validation/ValidationResult.cs b/allstarr/Services/Validation/ValidationResult.cs +new file mode 100644 +index 0000000..7293f8c +--- /dev/null ++++ b/allstarr/Services/Validation/ValidationResult.cs +@@ -0,0 +1,69 @@ ++namespace allstarr.Services.Validation; ++ ++/// ++/// Result of a startup validation operation ++/// ++public class ValidationResult ++{ ++ /// ++ /// Indicates whether the validation was successful ++ /// ++ public bool IsValid { get; set; } ++ ++ /// ++ /// Short status message (e.g., "VALID", "INVALID", "TIMEOUT", "NOT CONFIGURED") ++ /// ++ public string Status { get; set; } = string.Empty; ++ ++ /// ++ /// Detailed information about the validation result ++ /// ++ public string? Details { get; set; } ++ ++ /// ++ /// Color to use when displaying the status in console ++ /// ++ public ConsoleColor StatusColor { get; set; } = ConsoleColor.White; ++ ++ /// ++ /// Additional metadata about the validation ++ /// ++ public Dictionary Metadata { get; set; } = new(); ++ ++ /// ++ /// Creates a successful validation result ++ /// ++ public static ValidationResult Success(string details, Dictionary? metadata = null) ++ { ++ return new ValidationResult ++ { ++ IsValid = true, ++ Status = "VALID", ++ StatusColor = ConsoleColor.Green, ++ Details = details, ++ Metadata = metadata ?? new() ++ }; ++ } ++ ++ /// ++ /// Creates a failed validation result ++ /// ++ public static ValidationResult Failure(string status, string details, ConsoleColor color = ConsoleColor.Red) ++ { ++ return new ValidationResult ++ { ++ IsValid = false, ++ Status = status, ++ StatusColor = color, ++ Details = details ++ }; ++ } ++ ++ /// ++ /// Creates a not configured validation result ++ /// ++ public static ValidationResult NotConfigured(string details) ++ { ++ return Failure("NOT CONFIGURED", details, ConsoleColor.Red); ++ } ++} +diff --git a/allstarr/allstarr.csproj b/allstarr/allstarr.csproj +new file mode 100644 +index 0000000..83c5b4d +--- /dev/null ++++ b/allstarr/allstarr.csproj +@@ -0,0 +1,18 @@ ++ ++ ++ ++ net9.0 ++ enable ++ enable ++ allstarr ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/allstarr/allstarr.http b/allstarr/allstarr.http +new file mode 100644 +index 0000000..1e4703a +--- /dev/null ++++ b/allstarr/allstarr.http +@@ -0,0 +1,6 @@ ++@allstarr_HostAddress = http://localhost:5274 ++ ++GET {{allstarr_HostAddress}}/weatherforecast/ ++Accept: application/json ++ ++### +diff --git a/octo-fiesta/appsettings.Development.json b/allstarr/appsettings.Development.json +similarity index 100% +rename from octo-fiesta/appsettings.Development.json +rename to allstarr/appsettings.Development.json +diff --git a/allstarr/appsettings.json b/allstarr/appsettings.json +new file mode 100644 +index 0000000..8fec627 +--- /dev/null ++++ b/allstarr/appsettings.json +@@ -0,0 +1,46 @@ ++{ ++ "Backend": { ++ "Type": "Subsonic" ++ }, ++ "Subsonic": { ++ "Url": "https://navidrome.local.bransonb.com", ++ "MusicService": "SquidWTF", ++ "ExplicitFilter": "All", ++ "DownloadMode": "Track", ++ "StorageMode": "Permanent", ++ "CacheDurationHours": 1, ++ "EnableExternalPlaylists": true ++ }, ++ "Jellyfin": { ++ "Url": "http://localhost:8096", ++ "ApiKey": "", ++ "UserId": "", ++ "LibraryId": "", ++ "MusicService": "SquidWTF", ++ "ExplicitFilter": "All", ++ "DownloadMode": "Track", ++ "StorageMode": "Permanent", ++ "CacheDurationHours": 1, ++ "EnableExternalPlaylists": true ++ }, ++ "Library": { ++ "DownloadPath": "./downloads" ++ }, ++ "Qobuz": { ++ "UserAuthToken": "your-qobuz-token", ++ "UserId": "your-qobuz-user-id", ++ "Quality": "FLAC" ++ }, ++ "Deezer": { ++ "Arl": "your-deezer-arl-token", ++ "ArlFallback": "", ++ "Quality": "FLAC" ++ }, ++ "SquidWTF": { ++ "Quality": "FLAC" ++ }, ++ "Redis": { ++ "Enabled": true, ++ "ConnectionString": "localhost:6379" ++ } ++} +diff --git a/apis/jellyfin-openapi-stable.json b/apis/jellyfin-openapi-stable.json +new file mode 100644 +index 0000000..0a45a30 +--- /dev/null ++++ b/apis/jellyfin-openapi-stable.json +@@ -0,0 +1,69435 @@ ++{ ++ "openapi": "3.0.1", ++ "info": { ++ "title": "Jellyfin API", ++ "version": "10.11.6", ++ "x-jellyfin-version": "10.11.6" ++ }, ++ "servers": [ ++ { ++ "url": "http://localhost" ++ } ++ ], ++ "paths": { ++ "/System/ActivityLog/Entries": { ++ "get": { ++ "tags": [ ++ "ActivityLog" ++ ], ++ "summary": "Gets activity log entries.", ++ "operationId": "GetLogEntries", ++ "parameters": [ ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minDate", ++ "in": "query", ++ "description": "Optional. The minimum date. Format = ISO.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "hasUserId", ++ "in": "query", ++ "description": "Optional. Filter log entries if it has user id, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Activity log returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ActivityLogEntryQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ActivityLogEntryQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ActivityLogEntryQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Auth/Keys": { ++ "get": { ++ "tags": [ ++ "ApiKey" ++ ], ++ "summary": "Get all keys.", ++ "operationId": "GetKeys", ++ "responses": { ++ "200": { ++ "description": "Api keys retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/AuthenticationInfoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/AuthenticationInfoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/AuthenticationInfoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "ApiKey" ++ ], ++ "summary": "Create a new api key.", ++ "operationId": "CreateKey", ++ "parameters": [ ++ { ++ "name": "app", ++ "in": "query", ++ "description": "Name of the app using the authentication key.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Api key created." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Auth/Keys/{key}": { ++ "delete": { ++ "tags": [ ++ "ApiKey" ++ ], ++ "summary": "Remove an api key.", ++ "operationId": "RevokeKey", ++ "parameters": [ ++ { ++ "name": "key", ++ "in": "path", ++ "description": "The access token to delete.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Api key deleted." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Artists": { ++ "get": { ++ "tags": [ ++ "Artists" ++ ], ++ "summary": "Gets all artists from a given item, folder, or the entire library.", ++ "operationId": "GetArtists", ++ "parameters": [ ++ { ++ "name": "minCommunityRating", ++ "in": "query", ++ "description": "Optional filter by minimum community rating.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "searchTerm", ++ "in": "query", ++ "description": "Optional. Search term.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "excludeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "filters", ++ "in": "query", ++ "description": "Optional. Specify additional filters to apply.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFilter" ++ } ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional filter by items that are marked as favorite, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "mediaTypes", ++ "in": "query", ++ "description": "Optional filter by MediaType. Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ } ++ }, ++ { ++ "name": "genres", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "genreIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "officialRatings", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "tags", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "years", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional, include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional, the max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "person", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified person.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "personIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified person ids.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "personTypes", ++ "in": "query", ++ "description": "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "studios", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "studioIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "nameStartsWithOrGreater", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameStartsWith", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameLessThan", ++ "in": "query", ++ "description": "Optional filter by items whose name is equally or lesser than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Sort Order - Ascending,Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional, include image information in output.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Artists returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Artists/{name}": { ++ "get": { ++ "tags": [ ++ "Artists" ++ ], ++ "summary": "Gets an artist by name.", ++ "operationId": "GetArtistByName", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Studio name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Artist returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Artists/AlbumArtists": { ++ "get": { ++ "tags": [ ++ "Artists" ++ ], ++ "summary": "Gets all album artists from a given item, folder, or the entire library.", ++ "operationId": "GetAlbumArtists", ++ "parameters": [ ++ { ++ "name": "minCommunityRating", ++ "in": "query", ++ "description": "Optional filter by minimum community rating.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "searchTerm", ++ "in": "query", ++ "description": "Optional. Search term.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "excludeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "filters", ++ "in": "query", ++ "description": "Optional. Specify additional filters to apply.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFilter" ++ } ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional filter by items that are marked as favorite, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "mediaTypes", ++ "in": "query", ++ "description": "Optional filter by MediaType. Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ } ++ }, ++ { ++ "name": "genres", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "genreIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "officialRatings", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "tags", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "years", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional, include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional, the max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "person", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified person.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "personIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified person ids.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "personTypes", ++ "in": "query", ++ "description": "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "studios", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "studioIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "nameStartsWithOrGreater", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameStartsWith", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameLessThan", ++ "in": "query", ++ "description": "Optional filter by items whose name is equally or lesser than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Sort Order - Ascending,Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional, include image information in output.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Album artists returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Audio/{itemId}/stream": { ++ "get": { ++ "tags": [ ++ "Audio" ++ ], ++ "summary": "Gets an audio stream.", ++ "operationId": "GetAudioStream", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "query", ++ "description": "The audio container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Audio stream returned.", ++ "content": { ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Audio" ++ ], ++ "summary": "Gets an audio stream.", ++ "operationId": "HeadAudioStream", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "query", ++ "description": "The audio container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Audio stream returned.", ++ "content": { ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Audio/{itemId}/stream.{container}": { ++ "get": { ++ "tags": [ ++ "Audio" ++ ], ++ "summary": "Gets an audio stream.", ++ "operationId": "GetAudioStreamByContainer", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "path", ++ "description": "The audio container.", ++ "required": true, ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Audio stream returned.", ++ "content": { ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Audio" ++ ], ++ "summary": "Gets an audio stream.", ++ "operationId": "HeadAudioStreamByContainer", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "path", ++ "description": "The audio container.", ++ "required": true, ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Audio stream returned.", ++ "content": { ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Backup": { ++ "get": { ++ "tags": [ ++ "Backup" ++ ], ++ "summary": "Gets a list of all currently present backups in the backup directory.", ++ "operationId": "ListBackups", ++ "responses": { ++ "200": { ++ "description": "Backups available.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BackupManifestDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BackupManifestDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BackupManifestDto" ++ } ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to retrieve information.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Backup/Create": { ++ "post": { ++ "tags": [ ++ "Backup" ++ ], ++ "summary": "Creates a new Backup.", ++ "operationId": "CreateBackup", ++ "requestBody": { ++ "description": "The backup options.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BackupOptionsDto" ++ } ++ ], ++ "description": "Defines the optional contents of the backup archive." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BackupOptionsDto" ++ } ++ ], ++ "description": "Defines the optional contents of the backup archive." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BackupOptionsDto" ++ } ++ ], ++ "description": "Defines the optional contents of the backup archive." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "200": { ++ "description": "Backup created.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BackupManifestDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BackupManifestDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BackupManifestDto" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to retrieve information.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Backup/Manifest": { ++ "get": { ++ "tags": [ ++ "Backup" ++ ], ++ "summary": "Gets the descriptor from an existing archive is present.", ++ "operationId": "GetBackup", ++ "parameters": [ ++ { ++ "name": "path", ++ "in": "query", ++ "description": "The data to start a restore process.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Backup archive manifest.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BackupManifestDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BackupManifestDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BackupManifestDto" ++ } ++ } ++ } ++ }, ++ "204": { ++ "description": "Not a valid jellyfin Archive." ++ }, ++ "404": { ++ "description": "Not a valid path.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to retrieve information.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Backup/Restore": { ++ "post": { ++ "tags": [ ++ "Backup" ++ ], ++ "summary": "Restores to a backup by restarting the server and applying the backup.", ++ "operationId": "StartRestoreBackup", ++ "requestBody": { ++ "description": "The data to start a restore process.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BackupRestoreRequestDto" ++ } ++ ], ++ "description": "Defines properties used to start a restore process." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BackupRestoreRequestDto" ++ } ++ ], ++ "description": "Defines properties used to start a restore process." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BackupRestoreRequestDto" ++ } ++ ], ++ "description": "Defines properties used to start a restore process." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Backup restore started." ++ }, ++ "404": { ++ "description": "Not Found", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to retrieve information.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Branding/Configuration": { ++ "get": { ++ "tags": [ ++ "Branding" ++ ], ++ "summary": "Gets branding configuration.", ++ "operationId": "GetBrandingOptions", ++ "responses": { ++ "200": { ++ "description": "Branding configuration returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BrandingOptionsDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BrandingOptionsDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BrandingOptionsDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Branding/Css": { ++ "get": { ++ "tags": [ ++ "Branding" ++ ], ++ "summary": "Gets branding css.", ++ "operationId": "GetBrandingCss", ++ "responses": { ++ "200": { ++ "description": "Branding css returned.", ++ "content": { ++ "text/css": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "string" ++ } ++ } ++ } ++ }, ++ "204": { ++ "description": "No branding css configured." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Branding/Css.css": { ++ "get": { ++ "tags": [ ++ "Branding" ++ ], ++ "summary": "Gets branding css.", ++ "operationId": "GetBrandingCss_2", ++ "responses": { ++ "200": { ++ "description": "Branding css returned.", ++ "content": { ++ "text/css": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "string" ++ } ++ } ++ } ++ }, ++ "204": { ++ "description": "No branding css configured." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Channels": { ++ "get": { ++ "tags": [ ++ "Channels" ++ ], ++ "summary": "Gets available channels.", ++ "operationId": "GetChannels", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User Id to filter by. Use System.Guid.Empty to not filter by user.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "supportsLatestItems", ++ "in": "query", ++ "description": "Optional. Filter by channels that support getting latest items.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "supportsMediaDeletion", ++ "in": "query", ++ "description": "Optional. Filter by channels that support media deletion.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional. Filter by channels that are favorite.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Channels returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Channels/{channelId}/Features": { ++ "get": { ++ "tags": [ ++ "Channels" ++ ], ++ "summary": "Get channel features.", ++ "operationId": "GetChannelFeatures", ++ "parameters": [ ++ { ++ "name": "channelId", ++ "in": "path", ++ "description": "Channel id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Channel features returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ChannelFeatures" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ChannelFeatures" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ChannelFeatures" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Channels/{channelId}/Items": { ++ "get": { ++ "tags": [ ++ "Channels" ++ ], ++ "summary": "Get channel items.", ++ "operationId": "GetChannelItems", ++ "parameters": [ ++ { ++ "name": "channelId", ++ "in": "path", ++ "description": "Channel Id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "folderId", ++ "in": "query", ++ "description": "Optional. Folder Id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. User Id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Optional. Sort Order - Ascending,Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ }, ++ { ++ "name": "filters", ++ "in": "query", ++ "description": "Optional. Specify additional filters to apply.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFilter" ++ } ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Channel items returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Channels/Features": { ++ "get": { ++ "tags": [ ++ "Channels" ++ ], ++ "summary": "Get all channel features.", ++ "operationId": "GetAllChannelFeatures", ++ "responses": { ++ "200": { ++ "description": "All channel features returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ChannelFeatures" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ChannelFeatures" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ChannelFeatures" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Channels/Items/Latest": { ++ "get": { ++ "tags": [ ++ "Channels" ++ ], ++ "summary": "Gets latest channel items.", ++ "operationId": "GetLatestChannelItems", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. User Id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "filters", ++ "in": "query", ++ "description": "Optional. Specify additional filters to apply.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFilter" ++ } ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "channelIds", ++ "in": "query", ++ "description": "Optional. Specify one or more channel id's, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Latest channel items returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/ClientLog/Document": { ++ "post": { ++ "tags": [ ++ "ClientLog" ++ ], ++ "summary": "Upload a document.", ++ "operationId": "LogFile", ++ "requestBody": { ++ "content": { ++ "text/plain": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "responses": { ++ "200": { ++ "description": "Document saved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ClientLogDocumentResponseDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ClientLogDocumentResponseDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ClientLogDocumentResponseDto" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "Event logging disabled.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "413": { ++ "description": "Upload size too large.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Collections": { ++ "post": { ++ "tags": [ ++ "Collection" ++ ], ++ "summary": "Creates a new collection.", ++ "operationId": "CreateCollection", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "query", ++ "description": "The name of the collection.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "ids", ++ "in": "query", ++ "description": "Item Ids to add to the collection.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Optional. Create the collection within a specific folder.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "isLocked", ++ "in": "query", ++ "description": "Whether or not to lock the new collection.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Collection created.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/CollectionCreationResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/CollectionCreationResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/CollectionCreationResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "CollectionManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Collections/{collectionId}/Items": { ++ "post": { ++ "tags": [ ++ "Collection" ++ ], ++ "summary": "Adds items to a collection.", ++ "operationId": "AddToCollection", ++ "parameters": [ ++ { ++ "name": "collectionId", ++ "in": "path", ++ "description": "The collection id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "ids", ++ "in": "query", ++ "description": "Item ids, comma delimited.", ++ "required": true, ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Items added to collection." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "CollectionManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Collection" ++ ], ++ "summary": "Removes items from a collection.", ++ "operationId": "RemoveFromCollection", ++ "parameters": [ ++ { ++ "name": "collectionId", ++ "in": "path", ++ "description": "The collection id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "ids", ++ "in": "query", ++ "description": "Item ids, comma delimited.", ++ "required": true, ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Items removed from collection." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "CollectionManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Configuration": { ++ "get": { ++ "tags": [ ++ "Configuration" ++ ], ++ "summary": "Gets application configuration.", ++ "operationId": "GetConfiguration", ++ "responses": { ++ "200": { ++ "description": "Application configuration returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ServerConfiguration" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ServerConfiguration" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ServerConfiguration" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Configuration" ++ ], ++ "summary": "Updates application configuration.", ++ "operationId": "UpdateConfiguration", ++ "requestBody": { ++ "description": "Configuration.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ServerConfiguration" ++ } ++ ], ++ "description": "Represents the server configuration." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ServerConfiguration" ++ } ++ ], ++ "description": "Represents the server configuration." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ServerConfiguration" ++ } ++ ], ++ "description": "Represents the server configuration." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Configuration updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Configuration/{key}": { ++ "get": { ++ "tags": [ ++ "Configuration" ++ ], ++ "summary": "Gets a named configuration.", ++ "operationId": "GetNamedConfiguration", ++ "parameters": [ ++ { ++ "name": "key", ++ "in": "path", ++ "description": "Configuration key.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Configuration returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Configuration" ++ ], ++ "summary": "Updates named configuration.", ++ "operationId": "UpdateNamedConfiguration", ++ "parameters": [ ++ { ++ "name": "key", ++ "in": "path", ++ "description": "Configuration key.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "Configuration.", ++ "content": { ++ "application/json": { ++ "schema": { } ++ }, ++ "text/json": { ++ "schema": { } ++ }, ++ "application/*+json": { ++ "schema": { } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Named configuration updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Configuration/Branding": { ++ "post": { ++ "tags": [ ++ "Configuration" ++ ], ++ "summary": "Updates branding configuration.", ++ "operationId": "UpdateBrandingConfiguration", ++ "requestBody": { ++ "description": "Branding configuration.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BrandingOptionsDto" ++ } ++ ], ++ "description": "The branding options DTO for API use.\r\nThis DTO excludes SplashscreenLocation to prevent it from being updated via API." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BrandingOptionsDto" ++ } ++ ], ++ "description": "The branding options DTO for API use.\r\nThis DTO excludes SplashscreenLocation to prevent it from being updated via API." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BrandingOptionsDto" ++ } ++ ], ++ "description": "The branding options DTO for API use.\r\nThis DTO excludes SplashscreenLocation to prevent it from being updated via API." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Branding configuration updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Configuration/MetadataOptions/Default": { ++ "get": { ++ "tags": [ ++ "Configuration" ++ ], ++ "summary": "Gets a default MetadataOptions object.", ++ "operationId": "GetDefaultMetadataOptions", ++ "responses": { ++ "200": { ++ "description": "Metadata options returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/MetadataOptions" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/MetadataOptions" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/MetadataOptions" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/web/ConfigurationPage": { ++ "get": { ++ "tags": [ ++ "Dashboard" ++ ], ++ "summary": "Gets a dashboard configuration page.", ++ "operationId": "GetDashboardConfigurationPage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "query", ++ "description": "The name of the page.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "ConfigurationPage returned.", ++ "content": { ++ "text/html": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ }, ++ "application/x-javascript": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Plugin configuration page not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/web/ConfigurationPages": { ++ "get": { ++ "tags": [ ++ "Dashboard" ++ ], ++ "summary": "Gets the configuration pages.", ++ "operationId": "GetConfigurationPages", ++ "parameters": [ ++ { ++ "name": "enableInMainMenu", ++ "in": "query", ++ "description": "Whether to enable in the main menu.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "ConfigurationPages returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ConfigurationPageInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ConfigurationPageInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ConfigurationPageInfo" ++ } ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Server still loading.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Devices": { ++ "get": { ++ "tags": [ ++ "Devices" ++ ], ++ "summary": "Get Devices.", ++ "operationId": "GetDevices", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Gets or sets the user identifier.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Devices retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/DeviceInfoDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/DeviceInfoDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/DeviceInfoDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Devices" ++ ], ++ "summary": "Deletes a device.", ++ "operationId": "DeleteDevice", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "Device Id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Device deleted." ++ }, ++ "404": { ++ "description": "Device not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Devices/Info": { ++ "get": { ++ "tags": [ ++ "Devices" ++ ], ++ "summary": "Get info for a device.", ++ "operationId": "GetDeviceInfo", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "Device Id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Device info retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/DeviceInfoDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/DeviceInfoDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/DeviceInfoDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Device not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Devices/Options": { ++ "get": { ++ "tags": [ ++ "Devices" ++ ], ++ "summary": "Get options for a device.", ++ "operationId": "GetDeviceOptions", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "Device Id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Device options retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/DeviceOptionsDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/DeviceOptionsDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/DeviceOptionsDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Device not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Devices" ++ ], ++ "summary": "Update device options.", ++ "operationId": "UpdateDeviceOptions", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "Device Id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "Device Options.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DeviceOptionsDto" ++ } ++ ], ++ "description": "A dto representing custom options for a device." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DeviceOptionsDto" ++ } ++ ], ++ "description": "A dto representing custom options for a device." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DeviceOptionsDto" ++ } ++ ], ++ "description": "A dto representing custom options for a device." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Device options updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/DisplayPreferences/{displayPreferencesId}": { ++ "get": { ++ "tags": [ ++ "DisplayPreferences" ++ ], ++ "summary": "Get Display Preferences.", ++ "operationId": "GetDisplayPreferences", ++ "parameters": [ ++ { ++ "name": "displayPreferencesId", ++ "in": "path", ++ "description": "Display preferences id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "client", ++ "in": "query", ++ "description": "Client.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Display preferences retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/DisplayPreferencesDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/DisplayPreferencesDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/DisplayPreferencesDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "DisplayPreferences" ++ ], ++ "summary": "Update Display Preferences.", ++ "operationId": "UpdateDisplayPreferences", ++ "parameters": [ ++ { ++ "name": "displayPreferencesId", ++ "in": "path", ++ "description": "Display preferences id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User Id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "client", ++ "in": "query", ++ "description": "Client.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "New Display Preferences object.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DisplayPreferencesDto" ++ } ++ ], ++ "description": "Defines the display preferences for any item that supports them (usually Folders)." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DisplayPreferencesDto" ++ } ++ ], ++ "description": "Defines the display preferences for any item that supports them (usually Folders)." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DisplayPreferencesDto" ++ } ++ ], ++ "description": "Defines the display preferences for any item that supports them (usually Folders)." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Display preferences updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}": { ++ "get": { ++ "tags": [ ++ "DynamicHls" ++ ], ++ "summary": "Gets a video stream using HTTP live streaming.", ++ "operationId": "GetHlsAudioSegment", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentId", ++ "in": "path", ++ "description": "The segment id.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "path", ++ "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", ++ "required": true, ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "runtimeTicks", ++ "in": "query", ++ "description": "The position of the requested segment in ticks.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "actualSegmentLengthTicks", ++ "in": "query", ++ "description": "The length of the requested segment in ticks.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxStreamingBitrate", ++ "in": "query", ++ "description": "Optional. The maximum streaming bitrate.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Video stream returned.", ++ "content": { ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Audio/{itemId}/main.m3u8": { ++ "get": { ++ "tags": [ ++ "DynamicHls" ++ ], ++ "summary": "Gets an audio stream using HTTP live streaming.", ++ "operationId": "GetVariantHlsAudioPlaylist", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxStreamingBitrate", ++ "in": "query", ++ "description": "Optional. The maximum streaming bitrate.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Audio stream returned.", ++ "content": { ++ "application/x-mpegURL": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Audio/{itemId}/master.m3u8": { ++ "get": { ++ "tags": [ ++ "DynamicHls" ++ ], ++ "summary": "Gets an audio hls playlist stream.", ++ "operationId": "GetMasterHlsAudioPlaylist", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxStreamingBitrate", ++ "in": "query", ++ "description": "Optional. The maximum streaming bitrate.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAdaptiveBitrateStreaming", ++ "in": "query", ++ "description": "Enable adaptive bitrate streaming.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Audio stream returned.", ++ "content": { ++ "application/x-mpegURL": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "head": { ++ "tags": [ ++ "DynamicHls" ++ ], ++ "summary": "Gets an audio hls playlist stream.", ++ "operationId": "HeadMasterHlsAudioPlaylist", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxStreamingBitrate", ++ "in": "query", ++ "description": "Optional. The maximum streaming bitrate.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAdaptiveBitrateStreaming", ++ "in": "query", ++ "description": "Enable adaptive bitrate streaming.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Audio stream returned.", ++ "content": { ++ "application/x-mpegURL": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}": { ++ "get": { ++ "tags": [ ++ "DynamicHls" ++ ], ++ "summary": "Gets a video stream using HTTP live streaming.", ++ "operationId": "GetHlsVideoSegment", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentId", ++ "in": "path", ++ "description": "The segment id.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "path", ++ "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", ++ "required": true, ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "runtimeTicks", ++ "in": "query", ++ "description": "The position of the requested segment in ticks.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "actualSegmentLengthTicks", ++ "in": "query", ++ "description": "The length of the requested segment in ticks.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The desired segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. The maximum horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. The maximum vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "alwaysBurnInSubtitleWhenTranscoding", ++ "in": "query", ++ "description": "Whether to always burn in subtitles when transcoding.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Video stream returned.", ++ "content": { ++ "video/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/live.m3u8": { ++ "get": { ++ "tags": [ ++ "DynamicHls" ++ ], ++ "summary": "Gets a hls live stream.", ++ "operationId": "GetLiveHlsStream", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "query", ++ "description": "The audio container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. The max width.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. The max height.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableSubtitlesInManifest", ++ "in": "query", ++ "description": "Optional. Whether to enable subtitles in the manifest.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "alwaysBurnInSubtitleWhenTranscoding", ++ "in": "query", ++ "description": "Whether to always burn in subtitles when transcoding.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Hls live stream retrieved.", ++ "content": { ++ "application/x-mpegURL": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/main.m3u8": { ++ "get": { ++ "tags": [ ++ "DynamicHls" ++ ], ++ "summary": "Gets a video stream using HTTP live streaming.", ++ "operationId": "GetVariantHlsVideoPlaylist", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. The maximum horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. The maximum vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "alwaysBurnInSubtitleWhenTranscoding", ++ "in": "query", ++ "description": "Whether to always burn in subtitles when transcoding.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Video stream returned.", ++ "content": { ++ "application/x-mpegURL": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/master.m3u8": { ++ "get": { ++ "tags": [ ++ "DynamicHls" ++ ], ++ "summary": "Gets a video hls playlist stream.", ++ "operationId": "GetMasterHlsVideoPlaylist", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. The maximum horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. The maximum vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAdaptiveBitrateStreaming", ++ "in": "query", ++ "description": "Enable adaptive bitrate streaming.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "enableTrickplay", ++ "in": "query", ++ "description": "Enable trickplay image playlists being added to master playlist.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "alwaysBurnInSubtitleWhenTranscoding", ++ "in": "query", ++ "description": "Whether to always burn in subtitles when transcoding.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Video stream returned.", ++ "content": { ++ "application/x-mpegURL": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "head": { ++ "tags": [ ++ "DynamicHls" ++ ], ++ "summary": "Gets a video hls playlist stream.", ++ "operationId": "HeadMasterHlsVideoPlaylist", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. The maximum horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. The maximum vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAdaptiveBitrateStreaming", ++ "in": "query", ++ "description": "Enable adaptive bitrate streaming.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "enableTrickplay", ++ "in": "query", ++ "description": "Enable trickplay image playlists being added to master playlist.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "alwaysBurnInSubtitleWhenTranscoding", ++ "in": "query", ++ "description": "Whether to always burn in subtitles when transcoding.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Video stream returned.", ++ "content": { ++ "application/x-mpegURL": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Environment/DefaultDirectoryBrowser": { ++ "get": { ++ "tags": [ ++ "Environment" ++ ], ++ "summary": "Get Default directory browser.", ++ "operationId": "GetDefaultDirectoryBrowser", ++ "responses": { ++ "200": { ++ "description": "Default directory browser returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/DefaultDirectoryBrowserInfoDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/DefaultDirectoryBrowserInfoDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/DefaultDirectoryBrowserInfoDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Environment/DirectoryContents": { ++ "get": { ++ "tags": [ ++ "Environment" ++ ], ++ "summary": "Gets the contents of a given directory in the file system.", ++ "operationId": "GetDirectoryContents", ++ "parameters": [ ++ { ++ "name": "path", ++ "in": "query", ++ "description": "The path.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "includeFiles", ++ "in": "query", ++ "description": "An optional filter to include or exclude files from the results. true/false.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "includeDirectories", ++ "in": "query", ++ "description": "An optional filter to include or exclude folders from the results. true/false.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Directory contents returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FileSystemEntryInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FileSystemEntryInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FileSystemEntryInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Environment/Drives": { ++ "get": { ++ "tags": [ ++ "Environment" ++ ], ++ "summary": "Gets available drives from the server's file system.", ++ "operationId": "GetDrives", ++ "responses": { ++ "200": { ++ "description": "List of entries returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FileSystemEntryInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FileSystemEntryInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FileSystemEntryInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Environment/NetworkShares": { ++ "get": { ++ "tags": [ ++ "Environment" ++ ], ++ "summary": "Gets network paths.", ++ "operationId": "GetNetworkShares", ++ "responses": { ++ "200": { ++ "description": "Empty array returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FileSystemEntryInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FileSystemEntryInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FileSystemEntryInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Environment/ParentPath": { ++ "get": { ++ "tags": [ ++ "Environment" ++ ], ++ "summary": "Gets the parent path of a given path.", ++ "operationId": "GetParentPath", ++ "parameters": [ ++ { ++ "name": "path", ++ "in": "query", ++ "description": "The path.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "string" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Environment/ValidatePath": { ++ "post": { ++ "tags": [ ++ "Environment" ++ ], ++ "summary": "Validates path.", ++ "operationId": "ValidatePath", ++ "requestBody": { ++ "description": "Validate request object.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ValidatePathDto" ++ } ++ ], ++ "description": "Validate path object." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ValidatePathDto" ++ } ++ ], ++ "description": "Validate path object." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ValidatePathDto" ++ } ++ ], ++ "description": "Validate path object." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Path validated." ++ }, ++ "404": { ++ "description": "Path not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/Filters": { ++ "get": { ++ "tags": [ ++ "Filter" ++ ], ++ "summary": "Gets legacy query filters.", ++ "operationId": "GetQueryFiltersLegacy", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Optional. Parent id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "mediaTypes", ++ "in": "query", ++ "description": "Optional. Filter by MediaType. Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Legacy filters retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/QueryFiltersLegacy" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/QueryFiltersLegacy" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/QueryFiltersLegacy" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/Filters2": { ++ "get": { ++ "tags": [ ++ "Filter" ++ ], ++ "summary": "Gets query filters.", ++ "operationId": "GetQueryFilters", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "isAiring", ++ "in": "query", ++ "description": "Optional. Is item airing.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isMovie", ++ "in": "query", ++ "description": "Optional. Is item movie.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSports", ++ "in": "query", ++ "description": "Optional. Is item sports.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isKids", ++ "in": "query", ++ "description": "Optional. Is item kids.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isNews", ++ "in": "query", ++ "description": "Optional. Is item news.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSeries", ++ "in": "query", ++ "description": "Optional. Is item series.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "recursive", ++ "in": "query", ++ "description": "Optional. Search recursive.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Filters retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/QueryFilters" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/QueryFilters" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/QueryFilters" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Genres": { ++ "get": { ++ "tags": [ ++ "Genres" ++ ], ++ "summary": "Gets all genres from a given item, folder, or the entire library.", ++ "operationId": "GetGenres", ++ "parameters": [ ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "searchTerm", ++ "in": "query", ++ "description": "The search term.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "excludeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional filter by items that are marked as favorite, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional, the max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "nameStartsWithOrGreater", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameStartsWith", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameLessThan", ++ "in": "query", ++ "description": "Optional filter by items whose name is equally or lesser than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Sort Order - Ascending,Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional, include image information in output.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Optional. Include total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Genres returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Genres/{genreName}": { ++ "get": { ++ "tags": [ ++ "Genres" ++ ], ++ "summary": "Gets a genre, by name.", ++ "operationId": "GetGenre", ++ "parameters": [ ++ { ++ "name": "genreName", ++ "in": "path", ++ "description": "The genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Genres returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Audio/{itemId}/hls/{segmentId}/stream.aac": { ++ "get": { ++ "tags": [ ++ "HlsSegment" ++ ], ++ "summary": "Gets the specified audio segment for an audio item.", ++ "operationId": "GetHlsAudioSegmentLegacyAac", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentId", ++ "in": "path", ++ "description": "The segment id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Hls audio segment returned.", ++ "content": { ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Audio/{itemId}/hls/{segmentId}/stream.mp3": { ++ "get": { ++ "tags": [ ++ "HlsSegment" ++ ], ++ "summary": "Gets the specified audio segment for an audio item.", ++ "operationId": "GetHlsAudioSegmentLegacyMp3", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentId", ++ "in": "path", ++ "description": "The segment id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Hls audio segment returned.", ++ "content": { ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}": { ++ "get": { ++ "tags": [ ++ "HlsSegment" ++ ], ++ "summary": "Gets a hls video segment.", ++ "operationId": "GetHlsVideoSegmentLegacy", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentId", ++ "in": "path", ++ "description": "The segment id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "path", ++ "description": "The segment container.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Hls video segment returned.", ++ "content": { ++ "video/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Hls segment not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Videos/{itemId}/hls/{playlistId}/stream.m3u8": { ++ "get": { ++ "tags": [ ++ "HlsSegment" ++ ], ++ "summary": "Gets a hls video playlist.", ++ "operationId": "GetHlsPlaylistLegacy", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The video id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Hls video playlist returned.", ++ "content": { ++ "application/x-mpegURL": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/ActiveEncodings": { ++ "delete": { ++ "tags": [ ++ "HlsSegment" ++ ], ++ "summary": "Stops an active encoding.", ++ "operationId": "StopEncodingProcess", ++ "parameters": [ ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Encoding stopped successfully." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Artists/{name}/Images/{imageType}/{imageIndex}": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get artist image by name.", ++ "operationId": "GetArtistImage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Artist name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get artist image by name.", ++ "operationId": "HeadArtistImage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Artist name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Branding/Splashscreen": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Generates or gets the splashscreen.", ++ "operationId": "GetSplashscreen", ++ "parameters": [ ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Splashscreen returned successfully.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "post": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Uploads a custom splashscreen.\r\nThe body is expected to the image contents base64 encoded.", ++ "operationId": "UploadCustomSplashscreen", ++ "requestBody": { ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Successfully uploaded new splashscreen." ++ }, ++ "400": { ++ "description": "Error reading MimeType from uploaded image.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to upload splashscreen..", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Delete a custom splashscreen.", ++ "operationId": "DeleteCustomSplashscreen", ++ "responses": { ++ "204": { ++ "description": "Successfully deleted the custom splashscreen." ++ }, ++ "403": { ++ "description": "User does not have permission to delete splashscreen.." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Genres/{name}/Images/{imageType}": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get genre image by name.", ++ "operationId": "GetGenreImage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "Image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get genre image by name.", ++ "operationId": "HeadGenreImage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "Image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Genres/{name}/Images/{imageType}/{imageIndex}": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get genre image by name.", ++ "operationId": "GetGenreImageByIndex", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get genre image by name.", ++ "operationId": "HeadGenreImageByIndex", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Items/{itemId}/Images": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get item image infos.", ++ "operationId": "GetItemImageInfos", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Item images returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageInfo" ++ } ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/Images/{imageType}": { ++ "delete": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Delete an item's image.", ++ "operationId": "DeleteItemImage", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "The image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Image deleted." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Set item image.", ++ "operationId": "SetItemImage", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ } ++ ], ++ "requestBody": { ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Image saved." ++ }, ++ "400": { ++ "description": "Bad Request", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Gets the item's image.", ++ "operationId": "GetItemImage", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Drawing.ImageFormat of the returned image.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "Image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Gets the item's image.", ++ "operationId": "HeadItemImage", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Drawing.ImageFormat of the returned image.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "Image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Items/{itemId}/Images/{imageType}/{imageIndex}": { ++ "delete": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Delete an item's image.", ++ "operationId": "DeleteItemImageByIndex", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "The image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Image deleted." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Set item image.", ++ "operationId": "SetItemImageByIndex", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "(Unused) Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "requestBody": { ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Image saved." ++ }, ++ "400": { ++ "description": "Bad Request", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Gets the item's image.", ++ "operationId": "GetItemImageByIndex", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Drawing.ImageFormat of the returned image.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Gets the item's image.", ++ "operationId": "HeadItemImageByIndex", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Drawing.ImageFormat of the returned image.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Gets the item's image.", ++ "operationId": "GetItemImage2", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "path", ++ "description": "The maximum image width to return.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "path", ++ "description": "The maximum image height to return.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "path", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "path", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ], ++ "description": "Enum ImageOutputFormat." ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "path", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "required": true, ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "path", ++ "description": "Optional. Unplayed count overlay to render.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Gets the item's image.", ++ "operationId": "HeadItemImage2", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "path", ++ "description": "The maximum image width to return.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "path", ++ "description": "The maximum image height to return.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "path", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "path", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ], ++ "description": "Enum ImageOutputFormat." ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "path", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "required": true, ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "path", ++ "description": "Optional. Unplayed count overlay to render.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Items/{itemId}/Images/{imageType}/{imageIndex}/Index": { ++ "post": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Updates the index for an item image.", ++ "operationId": "UpdateItemImageIndex", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Old image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "newIndex", ++ "in": "query", ++ "description": "New image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Image index updated." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/MusicGenres/{name}/Images/{imageType}": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get music genre image by name.", ++ "operationId": "GetMusicGenreImage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Music genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "Image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get music genre image by name.", ++ "operationId": "HeadMusicGenreImage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Music genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "Image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/MusicGenres/{name}/Images/{imageType}/{imageIndex}": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get music genre image by name.", ++ "operationId": "GetMusicGenreImageByIndex", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Music genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get music genre image by name.", ++ "operationId": "HeadMusicGenreImageByIndex", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Music genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Persons/{name}/Images/{imageType}": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get person image by name.", ++ "operationId": "GetPersonImage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Person name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "Image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get person image by name.", ++ "operationId": "HeadPersonImage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Person name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "Image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Persons/{name}/Images/{imageType}/{imageIndex}": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get person image by name.", ++ "operationId": "GetPersonImageByIndex", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Person name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get person image by name.", ++ "operationId": "HeadPersonImageByIndex", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Person name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Studios/{name}/Images/{imageType}": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get studio image by name.", ++ "operationId": "GetStudioImage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Studio name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "Image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get studio image by name.", ++ "operationId": "HeadStudioImage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Studio name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "query", ++ "description": "Image index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Studios/{name}/Images/{imageType}/{imageIndex}": { ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get studio image by name.", ++ "operationId": "GetStudioImageByIndex", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Studio name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get studio image by name.", ++ "operationId": "HeadStudioImageByIndex", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Studio name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imageType", ++ "in": "path", ++ "description": "Image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageIndex", ++ "in": "path", ++ "description": "Image index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "The maximum image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "The maximum image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "percentPlayed", ++ "in": "query", ++ "description": "Optional. Percent to render for the percent played overlay.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "unplayedCount", ++ "in": "query", ++ "description": "Optional. Unplayed count overlay to render.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "The fixed image width to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "The fixed image height to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "quality", ++ "in": "query", ++ "description": "Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillWidth", ++ "in": "query", ++ "description": "Width of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fillHeight", ++ "in": "query", ++ "description": "Height of box to fill.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "blur", ++ "in": "query", ++ "description": "Optional. Blur image.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "backgroundColor", ++ "in": "query", ++ "description": "Optional. Apply a background color for transparent images.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "foregroundLayer", ++ "in": "query", ++ "description": "Optional. Apply a foreground layer on top of the image.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/UserImage": { ++ "post": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Sets the user image.", ++ "operationId": "PostUserImage", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User Id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "requestBody": { ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Image updated." ++ }, ++ "400": { ++ "description": "Bad Request", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to delete the image.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Delete the user's image.", ++ "operationId": "DeleteUserImage", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User Id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Image deleted." ++ }, ++ "403": { ++ "description": "User does not have permission to delete the image.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "get": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get user profile image.", ++ "operationId": "GetUserImage", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "400": { ++ "description": "User id not provided.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Image" ++ ], ++ "summary": "Get user profile image.", ++ "operationId": "HeadUserImage", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "Optional. Supply the cache tag from the item object to receive strong caching headers.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "Determines the output format of the image - original,gif,jpg,png.", ++ "schema": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageFormat" ++ } ++ ] ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Image stream returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "400": { ++ "description": "User id not provided.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Albums/{itemId}/InstantMix": { ++ "get": { ++ "tags": [ ++ "InstantMix" ++ ], ++ "summary": "Creates an instant playlist based on a given album.", ++ "operationId": "GetInstantMixFromAlbum", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Instant playlist returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Artists/{itemId}/InstantMix": { ++ "get": { ++ "tags": [ ++ "InstantMix" ++ ], ++ "summary": "Creates an instant playlist based on a given artist.", ++ "operationId": "GetInstantMixFromArtists", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Instant playlist returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Artists/InstantMix": { ++ "get": { ++ "tags": [ ++ "InstantMix" ++ ], ++ "summary": "Creates an instant playlist based on a given artist.", ++ "operationId": "GetInstantMixFromArtists2", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Instant playlist returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/InstantMix": { ++ "get": { ++ "tags": [ ++ "InstantMix" ++ ], ++ "summary": "Creates an instant playlist based on a given item.", ++ "operationId": "GetInstantMixFromItem", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Instant playlist returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/MusicGenres/{name}/InstantMix": { ++ "get": { ++ "tags": [ ++ "InstantMix" ++ ], ++ "summary": "Creates an instant playlist based on a given genre.", ++ "operationId": "GetInstantMixFromMusicGenreByName", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "The genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Instant playlist returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/MusicGenres/InstantMix": { ++ "get": { ++ "tags": [ ++ "InstantMix" ++ ], ++ "summary": "Creates an instant playlist based on a given genre.", ++ "operationId": "GetInstantMixFromMusicGenreById", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Instant playlist returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Playlists/{itemId}/InstantMix": { ++ "get": { ++ "tags": [ ++ "InstantMix" ++ ], ++ "summary": "Creates an instant playlist based on a given playlist.", ++ "operationId": "GetInstantMixFromPlaylist", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Instant playlist returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Songs/{itemId}/InstantMix": { ++ "get": { ++ "tags": [ ++ "InstantMix" ++ ], ++ "summary": "Creates an instant playlist based on a given song.", ++ "operationId": "GetInstantMixFromSong", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Instant playlist returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/ExternalIdInfos": { ++ "get": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Get the item's external id info.", ++ "operationId": "GetExternalIdInfos", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "External id info retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ExternalIdInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ExternalIdInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ExternalIdInfo" ++ } ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/RemoteSearch/Apply/{itemId}": { ++ "post": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Applies search criteria to an item and refreshes metadata.", ++ "operationId": "ApplySearchCriteria", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "replaceAllImages", ++ "in": "query", ++ "description": "Optional. Whether or not to replace all images. Default: True.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The remote search result.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Item metadata refreshed." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/RemoteSearch/Book": { ++ "post": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Get book remote search.", ++ "operationId": "GetBookRemoteSearchResults", ++ "requestBody": { ++ "description": "Remote search query.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BookInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BookInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BookInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Book remote search executed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/RemoteSearch/BoxSet": { ++ "post": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Get box set remote search.", ++ "operationId": "GetBoxSetRemoteSearchResults", ++ "requestBody": { ++ "description": "Remote search query.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BoxSetInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BoxSetInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BoxSetInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Box set remote search executed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/RemoteSearch/Movie": { ++ "post": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Get movie remote search.", ++ "operationId": "GetMovieRemoteSearchResults", ++ "requestBody": { ++ "description": "Remote search query.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MovieInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MovieInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MovieInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Movie remote search executed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/RemoteSearch/MusicAlbum": { ++ "post": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Get music album remote search.", ++ "operationId": "GetMusicAlbumRemoteSearchResults", ++ "requestBody": { ++ "description": "Remote search query.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AlbumInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AlbumInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AlbumInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Music album remote search executed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/RemoteSearch/MusicArtist": { ++ "post": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Get music artist remote search.", ++ "operationId": "GetMusicArtistRemoteSearchResults", ++ "requestBody": { ++ "description": "Remote search query.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ArtistInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ArtistInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ArtistInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Music artist remote search executed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/RemoteSearch/MusicVideo": { ++ "post": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Get music video remote search.", ++ "operationId": "GetMusicVideoRemoteSearchResults", ++ "requestBody": { ++ "description": "Remote search query.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MusicVideoInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MusicVideoInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MusicVideoInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Music video remote search executed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/RemoteSearch/Person": { ++ "post": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Get person remote search.", ++ "operationId": "GetPersonRemoteSearchResults", ++ "requestBody": { ++ "description": "Remote search query.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PersonLookupInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PersonLookupInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PersonLookupInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Person remote search executed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/RemoteSearch/Series": { ++ "post": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Get series remote search.", ++ "operationId": "GetSeriesRemoteSearchResults", ++ "requestBody": { ++ "description": "Remote search query.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeriesInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeriesInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeriesInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Series remote search executed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/RemoteSearch/Trailer": { ++ "post": { ++ "tags": [ ++ "ItemLookup" ++ ], ++ "summary": "Get trailer remote search.", ++ "operationId": "GetTrailerRemoteSearchResults", ++ "requestBody": { ++ "description": "Remote search query.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TrailerInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TrailerInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TrailerInfoRemoteSearchQuery" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Trailer remote search executed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/Refresh": { ++ "post": { ++ "tags": [ ++ "ItemRefresh" ++ ], ++ "summary": "Refreshes metadata for an item.", ++ "operationId": "RefreshItem", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "metadataRefreshMode", ++ "in": "query", ++ "description": "(Optional) Specifies the metadata refresh mode.", ++ "schema": { ++ "enum": [ ++ "None", ++ "ValidationOnly", ++ "Default", ++ "FullRefresh" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MetadataRefreshMode" ++ } ++ ], ++ "default": "None" ++ } ++ }, ++ { ++ "name": "imageRefreshMode", ++ "in": "query", ++ "description": "(Optional) Specifies the image refresh mode.", ++ "schema": { ++ "enum": [ ++ "None", ++ "ValidationOnly", ++ "Default", ++ "FullRefresh" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MetadataRefreshMode" ++ } ++ ], ++ "default": "None" ++ } ++ }, ++ { ++ "name": "replaceAllMetadata", ++ "in": "query", ++ "description": "(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "replaceAllImages", ++ "in": "query", ++ "description": "(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "regenerateTrickplay", ++ "in": "query", ++ "description": "(Optional) Determines if trickplay images should be replaced. Only applicable if mode is FullRefresh.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Item metadata refresh queued." ++ }, ++ "404": { ++ "description": "Item to refresh not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items": { ++ "get": { ++ "tags": [ ++ "Items" ++ ], ++ "summary": "Gets items based on a query.", ++ "operationId": "GetItems", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id supplied as query parameter; this is required when not using an API key.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "maxOfficialRating", ++ "in": "query", ++ "description": "Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "hasThemeSong", ++ "in": "query", ++ "description": "Optional filter by items with theme songs.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasThemeVideo", ++ "in": "query", ++ "description": "Optional filter by items with theme videos.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasSubtitles", ++ "in": "query", ++ "description": "Optional filter by items with subtitles.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasSpecialFeature", ++ "in": "query", ++ "description": "Optional filter by items with special features.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasTrailer", ++ "in": "query", ++ "description": "Optional filter by items with trailers.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "adjacentTo", ++ "in": "query", ++ "description": "Optional. Return items that are siblings of a supplied item.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "indexNumber", ++ "in": "query", ++ "description": "Optional filter by index number.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "parentIndexNumber", ++ "in": "query", ++ "description": "Optional filter by parent index number.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "hasParentalRating", ++ "in": "query", ++ "description": "Optional filter by items that have or do not have a parental rating.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isHd", ++ "in": "query", ++ "description": "Optional filter by items that are HD or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "is4K", ++ "in": "query", ++ "description": "Optional filter by items that are 4K or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "locationTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LocationType" ++ } ++ } ++ }, ++ { ++ "name": "excludeLocationTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LocationType" ++ } ++ } ++ }, ++ { ++ "name": "isMissing", ++ "in": "query", ++ "description": "Optional filter by items that are missing episodes or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isUnaired", ++ "in": "query", ++ "description": "Optional filter by items that are unaired episodes or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "minCommunityRating", ++ "in": "query", ++ "description": "Optional filter by minimum community rating.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "minCriticRating", ++ "in": "query", ++ "description": "Optional filter by minimum critic rating.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "minPremiereDate", ++ "in": "query", ++ "description": "Optional. The minimum premiere date. Format = ISO.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "minDateLastSaved", ++ "in": "query", ++ "description": "Optional. The minimum last saved date. Format = ISO.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "minDateLastSavedForUser", ++ "in": "query", ++ "description": "Optional. The minimum last saved date for the current user. Format = ISO.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "maxPremiereDate", ++ "in": "query", ++ "description": "Optional. The maximum premiere date. Format = ISO.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "hasOverview", ++ "in": "query", ++ "description": "Optional filter by items that have an overview or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasImdbId", ++ "in": "query", ++ "description": "Optional filter by items that have an IMDb id or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasTmdbId", ++ "in": "query", ++ "description": "Optional filter by items that have a TMDb id or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasTvdbId", ++ "in": "query", ++ "description": "Optional filter by items that have a TVDb id or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isMovie", ++ "in": "query", ++ "description": "Optional filter for live tv movies.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSeries", ++ "in": "query", ++ "description": "Optional filter for live tv series.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isNews", ++ "in": "query", ++ "description": "Optional filter for live tv news.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isKids", ++ "in": "query", ++ "description": "Optional filter for live tv kids.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSports", ++ "in": "query", ++ "description": "Optional filter for live tv sports.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "excludeItemIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "recursive", ++ "in": "query", ++ "description": "When searching within folders, this determines whether or not the search will be recursive. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "searchTerm", ++ "in": "query", ++ "description": "Optional. Filter based on a search term.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Sort Order - Ascending, Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "excludeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "filters", ++ "in": "query", ++ "description": "Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFilter" ++ } ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional filter by items that are marked as favorite, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "mediaTypes", ++ "in": "query", ++ "description": "Optional filter by MediaType. Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ } ++ }, ++ { ++ "name": "imageTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "isPlayed", ++ "in": "query", ++ "description": "Optional filter by items that are played, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "genres", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "officialRatings", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "tags", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "years", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional, include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional, the max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "person", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified person.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "personIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified person id.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "personTypes", ++ "in": "query", ++ "description": "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "studios", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "artists", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "excludeArtistIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "artistIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified artist id.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "albumArtistIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified album artist id.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "contributingArtistIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "albums", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "albumIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "ids", ++ "in": "query", ++ "description": "Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "videoTypes", ++ "in": "query", ++ "description": "Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/VideoType" ++ } ++ } ++ }, ++ { ++ "name": "minOfficialRating", ++ "in": "query", ++ "description": "Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "isLocked", ++ "in": "query", ++ "description": "Optional filter by items that are locked.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isPlaceHolder", ++ "in": "query", ++ "description": "Optional filter by items that are placeholders.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasOfficialRating", ++ "in": "query", ++ "description": "Optional filter by items that have official ratings.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "collapseBoxSetItems", ++ "in": "query", ++ "description": "Whether or not to hide items behind their boxsets.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "minWidth", ++ "in": "query", ++ "description": "Optional. Filter by the minimum width of the item.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minHeight", ++ "in": "query", ++ "description": "Optional. Filter by the minimum height of the item.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. Filter by the maximum width of the item.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. Filter by the maximum height of the item.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "is3D", ++ "in": "query", ++ "description": "Optional filter by items that are 3D, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "seriesStatus", ++ "in": "query", ++ "description": "Optional filter by Series Status. Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SeriesStatus" ++ } ++ } ++ }, ++ { ++ "name": "nameStartsWithOrGreater", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameStartsWith", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameLessThan", ++ "in": "query", ++ "description": "Optional filter by items whose name is equally or lesser than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "studioIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "genreIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Optional. Enable the total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional, include image information in output.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Deletes items from the library and filesystem.", ++ "operationId": "DeleteItems", ++ "parameters": [ ++ { ++ "name": "ids", ++ "in": "query", ++ "description": "The item ids.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Items deleted." ++ }, ++ "401": { ++ "description": "Unauthorized access.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Not Found", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/UserItems/{itemId}/UserData": { ++ "get": { ++ "tags": [ ++ "Items" ++ ], ++ "summary": "Get Item User Data.", ++ "operationId": "GetItemUserData", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "return item user data.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item is not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Items" ++ ], ++ "summary": "Update Item User Data.", ++ "operationId": "UpdateItemUserData", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "New user data object.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateUserItemDataDto" ++ } ++ ], ++ "description": "This is used by the api to get information about a item user data." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateUserItemDataDto" ++ } ++ ], ++ "description": "This is used by the api to get information about a item user data." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateUserItemDataDto" ++ } ++ ], ++ "description": "This is used by the api to get information about a item user data." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "return updated user item data.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item is not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/UserItems/Resume": { ++ "get": { ++ "tags": [ ++ "Items" ++ ], ++ "summary": "Gets items based on a query.", ++ "operationId": "GetResumeItems", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "The start index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "The item limit.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "searchTerm", ++ "in": "query", ++ "description": "The search term.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "mediaTypes", ++ "in": "query", ++ "description": "Optional. Filter by MediaType. Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "excludeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Optional. Enable the total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "excludeActiveSessions", ++ "in": "query", ++ "description": "Optional. Whether to exclude the currently active sessions.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Items returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}": { ++ "post": { ++ "tags": [ ++ "ItemUpdate" ++ ], ++ "summary": "Updates an item.", ++ "operationId": "UpdateItem", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The new item properties.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ ], ++ "description": "This is strictly used as a data transfer object from the api layer.\r\nThis holds information about a BaseItem in a format that is convenient for the client." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ ], ++ "description": "This is strictly used as a data transfer object from the api layer.\r\nThis holds information about a BaseItem in a format that is convenient for the client." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ ], ++ "description": "This is strictly used as a data transfer object from the api layer.\r\nThis holds information about a BaseItem in a format that is convenient for the client." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Item updated." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Deletes an item from the library and filesystem.", ++ "operationId": "DeleteItem", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Item deleted." ++ }, ++ "401": { ++ "description": "Unauthorized access.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "get": { ++ "tags": [ ++ "UserLibrary" ++ ], ++ "summary": "Gets an item from a user's library.", ++ "operationId": "GetItem", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Item returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/ContentType": { ++ "post": { ++ "tags": [ ++ "ItemUpdate" ++ ], ++ "summary": "Updates an item's content type.", ++ "operationId": "UpdateItemContentType", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "contentType", ++ "in": "query", ++ "description": "The content type of the item.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Item content type updated." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/MetadataEditor": { ++ "get": { ++ "tags": [ ++ "ItemUpdate" ++ ], ++ "summary": "Gets metadata editor info for an item.", ++ "operationId": "GetMetadataEditorInfo", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Item metadata editor returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/MetadataEditorInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/MetadataEditorInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/MetadataEditorInfo" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Albums/{itemId}/Similar": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets similar items.", ++ "operationId": "GetSimilarAlbums", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "excludeArtistIds", ++ "in": "query", ++ "description": "Exclude artist ids.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Similar items returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Artists/{itemId}/Similar": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets similar items.", ++ "operationId": "GetSimilarArtists", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "excludeArtistIds", ++ "in": "query", ++ "description": "Exclude artist ids.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Similar items returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/Ancestors": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets all parents of an item.", ++ "operationId": "GetAncestors", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Item parents returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/CriticReviews": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets critic review for an item.", ++ "operationId": "GetCriticReviews", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Critic reviews returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/Download": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Downloads item media.", ++ "operationId": "GetDownload", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Media downloaded.", ++ "content": { ++ "video/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ }, ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "Download", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/File": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Get the original file of an item.", ++ "operationId": "GetFile", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "File stream returned.", ++ "content": { ++ "video/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ }, ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/Similar": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets similar items.", ++ "operationId": "GetSimilarItems", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "excludeArtistIds", ++ "in": "query", ++ "description": "Exclude artist ids.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Similar items returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/ThemeMedia": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Get theme songs and videos for an item.", ++ "operationId": "GetThemeMedia", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "inheritFromParent", ++ "in": "query", ++ "description": "Optional. Determines whether or not parent items should be searched for theme media.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Optional. Sort Order - Ascending, Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Theme songs and videos returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/AllThemeMediaResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/AllThemeMediaResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/AllThemeMediaResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/ThemeSongs": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Get theme songs for an item.", ++ "operationId": "GetThemeSongs", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "inheritFromParent", ++ "in": "query", ++ "description": "Optional. Determines whether or not parent items should be searched for theme media.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Optional. Sort Order - Ascending, Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Theme songs returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ThemeMediaResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ThemeMediaResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ThemeMediaResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/ThemeVideos": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Get theme videos for an item.", ++ "operationId": "GetThemeVideos", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "inheritFromParent", ++ "in": "query", ++ "description": "Optional. Determines whether or not parent items should be searched for theme media.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Optional. Sort Order - Ascending, Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Theme videos returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ThemeMediaResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ThemeMediaResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ThemeMediaResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/Counts": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Get item counts.", ++ "operationId": "GetItemCounts", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Get counts from a specific user's library.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional. Get counts of favorite items.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Item counts returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ItemCounts" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ItemCounts" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ItemCounts" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Libraries/AvailableOptions": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets the library options info.", ++ "operationId": "GetLibraryOptionsInfo", ++ "parameters": [ ++ { ++ "name": "libraryContentType", ++ "in": "query", ++ "description": "Library content type.", ++ "schema": { ++ "enum": [ ++ "unknown", ++ "movies", ++ "tvshows", ++ "music", ++ "musicvideos", ++ "trailers", ++ "homevideos", ++ "boxsets", ++ "books", ++ "photos", ++ "livetv", ++ "playlists", ++ "folders" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CollectionType" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "isNewLibrary", ++ "in": "query", ++ "description": "Whether this is a new library.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Library options info returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/LibraryOptionsResultDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LibraryOptionsResultDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LibraryOptionsResultDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrDefault", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/Media/Updated": { ++ "post": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Reports that new movies have been added by an external source.", ++ "operationId": "PostUpdatedMedia", ++ "requestBody": { ++ "description": "The update paths.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaUpdateInfoDto" ++ } ++ ], ++ "description": "Media Update Info Dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaUpdateInfoDto" ++ } ++ ], ++ "description": "Media Update Info Dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaUpdateInfoDto" ++ } ++ ], ++ "description": "Media Update Info Dto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Report success." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/MediaFolders": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets all user media folders.", ++ "operationId": "GetMediaFolders", ++ "parameters": [ ++ { ++ "name": "isHidden", ++ "in": "query", ++ "description": "Optional. Filter by folders that are marked hidden, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Media folders returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/Movies/Added": { ++ "post": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Reports that new movies have been added by an external source.", ++ "operationId": "PostAddedMovies", ++ "parameters": [ ++ { ++ "name": "tmdbId", ++ "in": "query", ++ "description": "The tmdbId.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imdbId", ++ "in": "query", ++ "description": "The imdbId.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Report success." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/Movies/Updated": { ++ "post": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Reports that new movies have been added by an external source.", ++ "operationId": "PostUpdatedMovies", ++ "parameters": [ ++ { ++ "name": "tmdbId", ++ "in": "query", ++ "description": "The tmdbId.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "imdbId", ++ "in": "query", ++ "description": "The imdbId.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Report success." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/PhysicalPaths": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets a list of physical paths from virtual folders.", ++ "operationId": "GetPhysicalPaths", ++ "responses": { ++ "200": { ++ "description": "Physical paths returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/Refresh": { ++ "post": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Starts a library scan.", ++ "operationId": "RefreshLibrary", ++ "responses": { ++ "204": { ++ "description": "Library scan started." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/Series/Added": { ++ "post": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Reports that new episodes of a series have been added by an external source.", ++ "operationId": "PostAddedSeries", ++ "parameters": [ ++ { ++ "name": "tvdbId", ++ "in": "query", ++ "description": "The tvdbId.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Report success." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/Series/Updated": { ++ "post": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Reports that new episodes of a series have been added by an external source.", ++ "operationId": "PostUpdatedSeries", ++ "parameters": [ ++ { ++ "name": "tvdbId", ++ "in": "query", ++ "description": "The tvdbId.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Report success." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Movies/{itemId}/Similar": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets similar items.", ++ "operationId": "GetSimilarMovies", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "excludeArtistIds", ++ "in": "query", ++ "description": "Exclude artist ids.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Similar items returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Shows/{itemId}/Similar": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets similar items.", ++ "operationId": "GetSimilarShows", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "excludeArtistIds", ++ "in": "query", ++ "description": "Exclude artist ids.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Similar items returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Trailers/{itemId}/Similar": { ++ "get": { ++ "tags": [ ++ "Library" ++ ], ++ "summary": "Gets similar items.", ++ "operationId": "GetSimilarTrailers", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "excludeArtistIds", ++ "in": "query", ++ "description": "Exclude artist ids.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Similar items returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/VirtualFolders": { ++ "get": { ++ "tags": [ ++ "LibraryStructure" ++ ], ++ "summary": "Gets all virtual folders.", ++ "operationId": "GetVirtualFolders", ++ "responses": { ++ "200": { ++ "description": "Virtual folders retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/VirtualFolderInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/VirtualFolderInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/VirtualFolderInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "LibraryStructure" ++ ], ++ "summary": "Adds a virtual folder.", ++ "operationId": "AddVirtualFolder", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "query", ++ "description": "The name of the virtual folder.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "collectionType", ++ "in": "query", ++ "description": "The type of the collection.", ++ "schema": { ++ "enum": [ ++ "movies", ++ "tvshows", ++ "music", ++ "musicvideos", ++ "homevideos", ++ "boxsets", ++ "books", ++ "mixed" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CollectionTypeOptions" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "paths", ++ "in": "query", ++ "description": "The paths of the virtual folder.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "refreshLibrary", ++ "in": "query", ++ "description": "Whether to refresh the library.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The library options.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AddVirtualFolderDto" ++ } ++ ], ++ "description": "Add virtual folder dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AddVirtualFolderDto" ++ } ++ ], ++ "description": "Add virtual folder dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AddVirtualFolderDto" ++ } ++ ], ++ "description": "Add virtual folder dto." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Folder added." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "LibraryStructure" ++ ], ++ "summary": "Removes a virtual folder.", ++ "operationId": "RemoveVirtualFolder", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "query", ++ "description": "The name of the folder.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "refreshLibrary", ++ "in": "query", ++ "description": "Whether to refresh the library.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Folder removed." ++ }, ++ "404": { ++ "description": "Folder not found." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/VirtualFolders/LibraryOptions": { ++ "post": { ++ "tags": [ ++ "LibraryStructure" ++ ], ++ "summary": "Update library options.", ++ "operationId": "UpdateLibraryOptions", ++ "requestBody": { ++ "description": "The library name and options.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateLibraryOptionsDto" ++ } ++ ], ++ "description": "Update library options dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateLibraryOptionsDto" ++ } ++ ], ++ "description": "Update library options dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateLibraryOptionsDto" ++ } ++ ], ++ "description": "Update library options dto." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Library updated." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/VirtualFolders/Name": { ++ "post": { ++ "tags": [ ++ "LibraryStructure" ++ ], ++ "summary": "Renames a virtual folder.", ++ "operationId": "RenameVirtualFolder", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "query", ++ "description": "The name of the virtual folder.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "newName", ++ "in": "query", ++ "description": "The new name.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "refreshLibrary", ++ "in": "query", ++ "description": "Whether to refresh the library.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Folder renamed." ++ }, ++ "404": { ++ "description": "Library doesn't exist.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "409": { ++ "description": "Library already exists.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/VirtualFolders/Paths": { ++ "post": { ++ "tags": [ ++ "LibraryStructure" ++ ], ++ "summary": "Add a media path to a library.", ++ "operationId": "AddMediaPath", ++ "parameters": [ ++ { ++ "name": "refreshLibrary", ++ "in": "query", ++ "description": "Whether to refresh the library.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The media path dto.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaPathDto" ++ } ++ ], ++ "description": "Media Path dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaPathDto" ++ } ++ ], ++ "description": "Media Path dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaPathDto" ++ } ++ ], ++ "description": "Media Path dto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Media path added." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "LibraryStructure" ++ ], ++ "summary": "Remove a media path.", ++ "operationId": "RemoveMediaPath", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "query", ++ "description": "The name of the library.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "path", ++ "in": "query", ++ "description": "The path to remove.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "refreshLibrary", ++ "in": "query", ++ "description": "Whether to refresh the library.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Media path removed." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Library/VirtualFolders/Paths/Update": { ++ "post": { ++ "tags": [ ++ "LibraryStructure" ++ ], ++ "summary": "Updates a media path.", ++ "operationId": "UpdateMediaPath", ++ "requestBody": { ++ "description": "The name of the library and path infos.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateMediaPathRequestDto" ++ } ++ ], ++ "description": "Update library options dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateMediaPathRequestDto" ++ } ++ ], ++ "description": "Update library options dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateMediaPathRequestDto" ++ } ++ ], ++ "description": "Update library options dto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Media path updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/ChannelMappingOptions": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Get channel mapping options.", ++ "operationId": "GetChannelMappingOptions", ++ "parameters": [ ++ { ++ "name": "providerId", ++ "in": "query", ++ "description": "Provider id.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Channel mapping options returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ChannelMappingOptionsDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ChannelMappingOptionsDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ChannelMappingOptionsDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/ChannelMappings": { ++ "post": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Set channel mappings.", ++ "operationId": "SetChannelMapping", ++ "requestBody": { ++ "description": "The set channel mapping dto.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetChannelMappingDto" ++ } ++ ], ++ "description": "Set channel mapping dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetChannelMappingDto" ++ } ++ ], ++ "description": "Set channel mapping dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetChannelMappingDto" ++ } ++ ], ++ "description": "Set channel mapping dto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Created channel mapping returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/TunerChannelMapping" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/TunerChannelMapping" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/TunerChannelMapping" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Channels": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets available live tv channels.", ++ "operationId": "GetLiveTvChannels", ++ "parameters": [ ++ { ++ "name": "type", ++ "in": "query", ++ "description": "Optional. Filter by channel type.", ++ "schema": { ++ "enum": [ ++ "TV", ++ "Radio" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ChannelType" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "isMovie", ++ "in": "query", ++ "description": "Optional. Filter for movies.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSeries", ++ "in": "query", ++ "description": "Optional. Filter for series.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isNews", ++ "in": "query", ++ "description": "Optional. Filter for news.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isKids", ++ "in": "query", ++ "description": "Optional. Filter for kids.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSports", ++ "in": "query", ++ "description": "Optional. Filter for sports.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional. Filter by channels that are favorites, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isLiked", ++ "in": "query", ++ "description": "Optional. Filter by channels that are liked, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isDisliked", ++ "in": "query", ++ "description": "Optional. Filter by channels that are disliked, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "\"Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Key to sort by.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Optional. Sort order.", ++ "schema": { ++ "enum": [ ++ "Ascending", ++ "Descending" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "enableFavoriteSorting", ++ "in": "query", ++ "description": "Optional. Incorporate favorite and like status into channel sorting.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "addCurrentProgram", ++ "in": "query", ++ "description": "Optional. Adds current program info to each channel.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Available live tv channels returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Channels/{channelId}": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets a live tv channel.", ++ "operationId": "GetChannel", ++ "parameters": [ ++ { ++ "name": "channelId", ++ "in": "path", ++ "description": "Channel id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Live tv channel returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/GuideInfo": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Get guide info.", ++ "operationId": "GetGuideInfo", ++ "responses": { ++ "200": { ++ "description": "Guide info returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/GuideInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/GuideInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/GuideInfo" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Info": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets available live tv services.", ++ "operationId": "GetLiveTvInfo", ++ "responses": { ++ "200": { ++ "description": "Available live tv services returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/LiveTvInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LiveTvInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LiveTvInfo" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/ListingProviders": { ++ "post": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Adds a listings provider.", ++ "operationId": "AddListingProvider", ++ "parameters": [ ++ { ++ "name": "pw", ++ "in": "query", ++ "description": "Password.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "validateListings", ++ "in": "query", ++ "description": "Validate listings.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "validateLogin", ++ "in": "query", ++ "description": "Validate login.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "New listings info.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ListingsProviderInfo" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ListingsProviderInfo" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ListingsProviderInfo" ++ } ++ ] ++ } ++ } ++ } ++ }, ++ "responses": { ++ "200": { ++ "description": "Created listings provider returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ListingsProviderInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ListingsProviderInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ListingsProviderInfo" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Delete listing provider.", ++ "operationId": "DeleteListingProvider", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "Listing provider id.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Listing provider deleted." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/ListingProviders/Default": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets default listings provider info.", ++ "operationId": "GetDefaultListingProvider", ++ "responses": { ++ "200": { ++ "description": "Default listings provider info returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ListingsProviderInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ListingsProviderInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ListingsProviderInfo" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/ListingProviders/Lineups": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets available lineups.", ++ "operationId": "GetLineups", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "Provider id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "type", ++ "in": "query", ++ "description": "Provider type.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "location", ++ "in": "query", ++ "description": "Location.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "country", ++ "in": "query", ++ "description": "Country.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Available lineups returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/ListingProviders/SchedulesDirect/Countries": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets available countries.", ++ "operationId": "GetSchedulesDirectCountries", ++ "responses": { ++ "200": { ++ "description": "Available countries returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/LiveRecordings/{recordingId}/stream": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets a live tv recording stream.", ++ "operationId": "GetLiveRecordingFile", ++ "parameters": [ ++ { ++ "name": "recordingId", ++ "in": "path", ++ "description": "Recording id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Recording stream returned.", ++ "content": { ++ "video/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Recording not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/LiveTv/LiveStreamFiles/{streamId}/stream.{container}": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets a live tv channel stream.", ++ "operationId": "GetLiveStreamFile", ++ "parameters": [ ++ { ++ "name": "streamId", ++ "in": "path", ++ "description": "Stream id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "path", ++ "description": "Container type.", ++ "required": true, ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Stream returned.", ++ "content": { ++ "video/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Stream not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/LiveTv/Programs": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets available live tv epgs.", ++ "operationId": "GetLiveTvPrograms", ++ "parameters": [ ++ { ++ "name": "channelIds", ++ "in": "query", ++ "description": "The channels to return guide information for.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "minStartDate", ++ "in": "query", ++ "description": "Optional. The minimum premiere start date.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "hasAired", ++ "in": "query", ++ "description": "Optional. Filter by programs that have completed airing, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isAiring", ++ "in": "query", ++ "description": "Optional. Filter by programs that are currently airing, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "maxStartDate", ++ "in": "query", ++ "description": "Optional. The maximum premiere start date.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "minEndDate", ++ "in": "query", ++ "description": "Optional. The minimum premiere end date.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "maxEndDate", ++ "in": "query", ++ "description": "Optional. The maximum premiere end date.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "isMovie", ++ "in": "query", ++ "description": "Optional. Filter for movies.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSeries", ++ "in": "query", ++ "description": "Optional. Filter for series.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isNews", ++ "in": "query", ++ "description": "Optional. Filter for news.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isKids", ++ "in": "query", ++ "description": "Optional. Filter for kids.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSports", ++ "in": "query", ++ "description": "Optional. Filter for sports.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited. Options: Name, StartDate.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Sort Order - Ascending,Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ }, ++ { ++ "name": "genres", ++ "in": "query", ++ "description": "The genres to return guide information for.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "genreIds", ++ "in": "query", ++ "description": "The genre ids to return guide information for.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "seriesTimerId", ++ "in": "query", ++ "description": "Optional. Filter by series timer id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "librarySeriesId", ++ "in": "query", ++ "description": "Optional. Filter by library series id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Retrieve total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Live tv epgs returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets available live tv epgs.", ++ "operationId": "GetPrograms", ++ "requestBody": { ++ "description": "Request body.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GetProgramsDto" ++ } ++ ], ++ "description": "Get programs dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GetProgramsDto" ++ } ++ ], ++ "description": "Get programs dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GetProgramsDto" ++ } ++ ], ++ "description": "Get programs dto." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "200": { ++ "description": "Live tv epgs returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Programs/{programId}": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets a live tv program.", ++ "operationId": "GetProgram", ++ "parameters": [ ++ { ++ "name": "programId", ++ "in": "path", ++ "description": "Program id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Program returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Programs/Recommended": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets recommended live tv epgs.", ++ "operationId": "GetRecommendedPrograms", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. filter by user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "isAiring", ++ "in": "query", ++ "description": "Optional. Filter by programs that are currently airing, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasAired", ++ "in": "query", ++ "description": "Optional. Filter by programs that have completed airing, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSeries", ++ "in": "query", ++ "description": "Optional. Filter for series.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isMovie", ++ "in": "query", ++ "description": "Optional. Filter for movies.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isNews", ++ "in": "query", ++ "description": "Optional. Filter for news.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isKids", ++ "in": "query", ++ "description": "Optional. Filter for kids.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSports", ++ "in": "query", ++ "description": "Optional. Filter for sports.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "genreIds", ++ "in": "query", ++ "description": "The genres to return guide information for.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Retrieve total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Recommended epgs returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Recordings": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets live tv recordings.", ++ "operationId": "GetRecordings", ++ "parameters": [ ++ { ++ "name": "channelId", ++ "in": "query", ++ "description": "Optional. Filter by channel id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "status", ++ "in": "query", ++ "description": "Optional. Filter by recording status.", ++ "schema": { ++ "enum": [ ++ "New", ++ "InProgress", ++ "Completed", ++ "Cancelled", ++ "ConflictedOk", ++ "ConflictedNotOk", ++ "Error" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RecordingStatus" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "isInProgress", ++ "in": "query", ++ "description": "Optional. Filter by recordings that are in progress, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "seriesTimerId", ++ "in": "query", ++ "description": "Optional. Filter by recordings belonging to a series timer.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isMovie", ++ "in": "query", ++ "description": "Optional. Filter for movies.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSeries", ++ "in": "query", ++ "description": "Optional. Filter for series.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isKids", ++ "in": "query", ++ "description": "Optional. Filter for kids.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSports", ++ "in": "query", ++ "description": "Optional. Filter for sports.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isNews", ++ "in": "query", ++ "description": "Optional. Filter for news.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isLibraryItem", ++ "in": "query", ++ "description": "Optional. Filter for is library item.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Optional. Return total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Live tv recordings returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Recordings/{recordingId}": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets a live tv recording.", ++ "operationId": "GetRecording", ++ "parameters": [ ++ { ++ "name": "recordingId", ++ "in": "path", ++ "description": "Recording id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Recording returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Deletes a live tv recording.", ++ "operationId": "DeleteRecording", ++ "parameters": [ ++ { ++ "name": "recordingId", ++ "in": "path", ++ "description": "Recording id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Recording deleted." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Recordings/Folders": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets recording folders.", ++ "operationId": "GetRecordingFolders", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Recording folders returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Recordings/Groups": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets live tv recording groups.", ++ "operationId": "GetRecordingGroups", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Recording groups returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Recordings/Groups/{groupId}": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Get recording group.", ++ "operationId": "GetRecordingGroup", ++ "parameters": [ ++ { ++ "name": "groupId", ++ "in": "path", ++ "description": "Group id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "404": { ++ "description": "Not Found", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Recordings/Series": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets live tv recording series.", ++ "operationId": "GetRecordingsSeries", ++ "parameters": [ ++ { ++ "name": "channelId", ++ "in": "query", ++ "description": "Optional. Filter by channel id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "groupId", ++ "in": "query", ++ "description": "Optional. Filter by recording group.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "status", ++ "in": "query", ++ "description": "Optional. Filter by recording status.", ++ "schema": { ++ "enum": [ ++ "New", ++ "InProgress", ++ "Completed", ++ "Cancelled", ++ "ConflictedOk", ++ "ConflictedNotOk", ++ "Error" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RecordingStatus" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "isInProgress", ++ "in": "query", ++ "description": "Optional. Filter by recordings that are in progress, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "seriesTimerId", ++ "in": "query", ++ "description": "Optional. Filter by recordings belonging to a series timer.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Optional. Return total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Live tv recordings returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/SeriesTimers": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets live tv series timers.", ++ "operationId": "GetSeriesTimers", ++ "parameters": [ ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Sort by SortName or Priority.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Optional. Sort in Ascending or Descending order.", ++ "schema": { ++ "enum": [ ++ "Ascending", ++ "Descending" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ ] ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Timers returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/SeriesTimerInfoDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SeriesTimerInfoDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SeriesTimerInfoDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Creates a live tv series timer.", ++ "operationId": "CreateSeriesTimer", ++ "requestBody": { ++ "description": "New series timer info.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ ], ++ "description": "Class SeriesTimerInfoDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ ], ++ "description": "Class SeriesTimerInfoDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ ], ++ "description": "Class SeriesTimerInfoDto." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Series timer info created." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/SeriesTimers/{timerId}": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets a live tv series timer.", ++ "operationId": "GetSeriesTimer", ++ "parameters": [ ++ { ++ "name": "timerId", ++ "in": "path", ++ "description": "Timer id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Series timer returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Series timer not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Cancels a live tv series timer.", ++ "operationId": "CancelSeriesTimer", ++ "parameters": [ ++ { ++ "name": "timerId", ++ "in": "path", ++ "description": "Timer id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Timer cancelled." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Updates a live tv series timer.", ++ "operationId": "UpdateSeriesTimer", ++ "parameters": [ ++ { ++ "name": "timerId", ++ "in": "path", ++ "description": "Timer id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "New series timer info.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ ], ++ "description": "Class SeriesTimerInfoDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ ], ++ "description": "Class SeriesTimerInfoDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ ], ++ "description": "Class SeriesTimerInfoDto." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Series timer updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Timers": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets the live tv timers.", ++ "operationId": "GetTimers", ++ "parameters": [ ++ { ++ "name": "channelId", ++ "in": "query", ++ "description": "Optional. Filter by channel id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "seriesTimerId", ++ "in": "query", ++ "description": "Optional. Filter by timers belonging to a series timer.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "isActive", ++ "in": "query", ++ "description": "Optional. Filter by timers that are active.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isScheduled", ++ "in": "query", ++ "description": "Optional. Filter by timers that are scheduled.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/TimerInfoDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/TimerInfoDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/TimerInfoDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Creates a live tv timer.", ++ "operationId": "CreateTimer", ++ "requestBody": { ++ "description": "New timer info.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TimerInfoDto" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TimerInfoDto" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TimerInfoDto" ++ } ++ ] ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Timer created." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Timers/{timerId}": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets a timer.", ++ "operationId": "GetTimer", ++ "parameters": [ ++ { ++ "name": "timerId", ++ "in": "path", ++ "description": "Timer id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Timer returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/TimerInfoDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/TimerInfoDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/TimerInfoDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Cancels a live tv timer.", ++ "operationId": "CancelTimer", ++ "parameters": [ ++ { ++ "name": "timerId", ++ "in": "path", ++ "description": "Timer id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Timer deleted." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Updates a live tv timer.", ++ "operationId": "UpdateTimer", ++ "parameters": [ ++ { ++ "name": "timerId", ++ "in": "path", ++ "description": "Timer id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "New timer info.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TimerInfoDto" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TimerInfoDto" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TimerInfoDto" ++ } ++ ] ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Timer updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Timers/Defaults": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Gets the default values for a new timer.", ++ "operationId": "GetDefaultTimer", ++ "parameters": [ ++ { ++ "name": "programId", ++ "in": "query", ++ "description": "Optional. To attach default values based on a program.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Default values returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/TunerHosts": { ++ "post": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Adds a tuner host.", ++ "operationId": "AddTunerHost", ++ "requestBody": { ++ "description": "New tuner host.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ ] ++ } ++ } ++ } ++ }, ++ "responses": { ++ "200": { ++ "description": "Created tuner host returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Deletes a tuner host.", ++ "operationId": "DeleteTunerHost", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "Tuner host id.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Tuner host deleted." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/TunerHosts/Types": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Get tuner host types.", ++ "operationId": "GetTunerHostTypes", ++ "responses": { ++ "200": { ++ "description": "Tuner host types returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Tuners/{tunerId}/Reset": { ++ "post": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Resets a tv tuner.", ++ "operationId": "ResetTuner", ++ "parameters": [ ++ { ++ "name": "tunerId", ++ "in": "path", ++ "description": "Tuner id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Tuner reset." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Tuners/Discover": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Discover tuners.", ++ "operationId": "DiscoverTuners", ++ "parameters": [ ++ { ++ "name": "newDevicesOnly", ++ "in": "query", ++ "description": "Only discover new tuners.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Tuners returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveTv/Tuners/Discvover": { ++ "get": { ++ "tags": [ ++ "LiveTv" ++ ], ++ "summary": "Discover tuners.", ++ "operationId": "DiscvoverTuners", ++ "parameters": [ ++ { ++ "name": "newDevicesOnly", ++ "in": "query", ++ "description": "Only discover new tuners.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Tuners returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LiveTvManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Localization/Countries": { ++ "get": { ++ "tags": [ ++ "Localization" ++ ], ++ "summary": "Gets known countries.", ++ "operationId": "GetCountries", ++ "responses": { ++ "200": { ++ "description": "Known countries returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CountryInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CountryInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CountryInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrDefault", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Localization/Cultures": { ++ "get": { ++ "tags": [ ++ "Localization" ++ ], ++ "summary": "Gets known cultures.", ++ "operationId": "GetCultures", ++ "responses": { ++ "200": { ++ "description": "Known cultures returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CultureDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CultureDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CultureDto" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrDefault", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Localization/Options": { ++ "get": { ++ "tags": [ ++ "Localization" ++ ], ++ "summary": "Gets localization options.", ++ "operationId": "GetLocalizationOptions", ++ "responses": { ++ "200": { ++ "description": "Localization options returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LocalizationOption" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LocalizationOption" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LocalizationOption" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrDefault", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Localization/ParentalRatings": { ++ "get": { ++ "tags": [ ++ "Localization" ++ ], ++ "summary": "Gets known parental ratings.", ++ "operationId": "GetParentalRatings", ++ "responses": { ++ "200": { ++ "description": "Known parental ratings returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ParentalRating" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ParentalRating" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ParentalRating" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrDefault", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Audio/{itemId}/Lyrics": { ++ "get": { ++ "tags": [ ++ "Lyrics" ++ ], ++ "summary": "Gets an item's lyrics.", ++ "operationId": "GetLyrics", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Lyrics returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Something went wrong. No Lyrics will be returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Lyrics" ++ ], ++ "summary": "Upload an external lyric file.", ++ "operationId": "UploadLyrics", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item the lyric belongs to.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fileName", ++ "in": "query", ++ "description": "Name of the file being uploaded.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "requestBody": { ++ "content": { ++ "text/plain": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "responses": { ++ "200": { ++ "description": "Lyrics uploaded.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ } ++ } ++ }, ++ "400": { ++ "description": "Error processing upload.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LyricManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Lyrics" ++ ], ++ "summary": "Deletes an external lyric file.", ++ "operationId": "DeleteLyrics", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Lyric deleted." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LyricManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Audio/{itemId}/RemoteSearch/Lyrics": { ++ "get": { ++ "tags": [ ++ "Lyrics" ++ ], ++ "summary": "Search remote lyrics.", ++ "operationId": "SearchRemoteLyrics", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Lyrics retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteLyricInfoDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteLyricInfoDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteLyricInfoDto" ++ } ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LyricManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Audio/{itemId}/RemoteSearch/Lyrics/{lyricId}": { ++ "post": { ++ "tags": [ ++ "Lyrics" ++ ], ++ "summary": "Downloads a remote lyric.", ++ "operationId": "DownloadRemoteLyrics", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "lyricId", ++ "in": "path", ++ "description": "The lyric id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Lyric downloaded.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LyricManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Providers/Lyrics/{lyricId}": { ++ "get": { ++ "tags": [ ++ "Lyrics" ++ ], ++ "summary": "Gets the remote lyrics.", ++ "operationId": "GetRemoteLyrics", ++ "parameters": [ ++ { ++ "name": "lyricId", ++ "in": "path", ++ "description": "The remote provider item id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "File returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Lyric not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LyricManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/PlaybackInfo": { ++ "get": { ++ "tags": [ ++ "MediaInfo" ++ ], ++ "summary": "Gets live playback media info for an item.", ++ "operationId": "GetPlaybackInfo", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Playback info returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaybackInfoResponse" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaybackInfoResponse" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaybackInfoResponse" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "MediaInfo" ++ ], ++ "summary": "Gets live playback media info for an item.", ++ "description": "For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.\r\nQuery parameters are obsolete.", ++ "operationId": "GetPostedPlaybackInfo", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "deprecated": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "maxStreamingBitrate", ++ "in": "query", ++ "description": "The maximum streaming bitrate.", ++ "deprecated": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "The start time in ticks.", ++ "deprecated": true, ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "The audio stream index.", ++ "deprecated": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "The subtitle stream index.", ++ "deprecated": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "The maximum number of audio channels.", ++ "deprecated": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media source id.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The livestream id.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "autoOpenLiveStream", ++ "in": "query", ++ "description": "Whether to auto open the livestream.", ++ "deprecated": true, ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableDirectPlay", ++ "in": "query", ++ "description": "Whether to enable direct play. Default: true.", ++ "deprecated": true, ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableDirectStream", ++ "in": "query", ++ "description": "Whether to enable direct stream. Default: true.", ++ "deprecated": true, ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableTranscoding", ++ "in": "query", ++ "description": "Whether to enable transcoding. Default: true.", ++ "deprecated": true, ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether to allow to copy the video stream. Default: true.", ++ "deprecated": true, ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether to allow to copy the audio stream. Default: true.", ++ "deprecated": true, ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The playback info.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackInfoDto" ++ } ++ ], ++ "description": "Playback info dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackInfoDto" ++ } ++ ], ++ "description": "Playback info dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackInfoDto" ++ } ++ ], ++ "description": "Playback info dto." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "200": { ++ "description": "Playback info returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaybackInfoResponse" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaybackInfoResponse" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaybackInfoResponse" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveStreams/Close": { ++ "post": { ++ "tags": [ ++ "MediaInfo" ++ ], ++ "summary": "Closes a media source.", ++ "operationId": "CloseLiveStream", ++ "parameters": [ ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The livestream id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Livestream closed." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/LiveStreams/Open": { ++ "post": { ++ "tags": [ ++ "MediaInfo" ++ ], ++ "summary": "Opens a media source.", ++ "operationId": "OpenLiveStream", ++ "parameters": [ ++ { ++ "name": "openToken", ++ "in": "query", ++ "description": "The open token.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "maxStreamingBitrate", ++ "in": "query", ++ "description": "The maximum streaming bitrate.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "The start time in ticks.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "The audio stream index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "The subtitle stream index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "The maximum number of audio channels.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "query", ++ "description": "The item id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "enableDirectPlay", ++ "in": "query", ++ "description": "Whether to enable direct play. Default: true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableDirectStream", ++ "in": "query", ++ "description": "Whether to enable direct stream. Default: true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "alwaysBurnInSubtitleWhenTranscoding", ++ "in": "query", ++ "description": "Always burn-in subtitle when transcoding.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The open live stream dto.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/OpenLiveStreamDto" ++ } ++ ], ++ "description": "Open live stream dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/OpenLiveStreamDto" ++ } ++ ], ++ "description": "Open live stream dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/OpenLiveStreamDto" ++ } ++ ], ++ "description": "Open live stream dto." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "200": { ++ "description": "Media source opened.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/LiveStreamResponse" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LiveStreamResponse" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/LiveStreamResponse" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Playback/BitrateTest": { ++ "get": { ++ "tags": [ ++ "MediaInfo" ++ ], ++ "summary": "Tests the network with a request with the size of the bitrate.", ++ "operationId": "GetBitrateTestBytes", ++ "parameters": [ ++ { ++ "name": "size", ++ "in": "query", ++ "description": "The bitrate. Defaults to 102400.", ++ "schema": { ++ "maximum": 100000000, ++ "minimum": 1, ++ "type": "integer", ++ "format": "int32", ++ "default": 102400 ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Test buffer returned.", ++ "content": { ++ "application/octet-stream": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/MediaSegments/{itemId}": { ++ "get": { ++ "tags": [ ++ "MediaSegments" ++ ], ++ "summary": "Gets all media segments based on an itemId.", ++ "operationId": "GetItemSegments", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The ItemId.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "includeSegmentTypes", ++ "in": "query", ++ "description": "Optional filter of requested segment types.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaSegmentType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/MediaSegmentDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/MediaSegmentDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/MediaSegmentDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Not Found", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Movies/Recommendations": { ++ "get": { ++ "tags": [ ++ "Movies" ++ ], ++ "summary": "Gets movie recommendations.", ++ "operationId": "GetMovieRecommendations", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. The fields to return.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "categoryLimit", ++ "in": "query", ++ "description": "The max number of categories to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32", ++ "default": 5 ++ } ++ }, ++ { ++ "name": "itemLimit", ++ "in": "query", ++ "description": "The max number of items to return per category.", ++ "schema": { ++ "type": "integer", ++ "format": "int32", ++ "default": 8 ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Movie recommendations returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RecommendationDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RecommendationDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RecommendationDto" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/MusicGenres": { ++ "get": { ++ "tags": [ ++ "MusicGenres" ++ ], ++ "summary": "Gets all music genres from a given item, folder, or the entire library.", ++ "operationId": "GetMusicGenres", ++ "parameters": [ ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "searchTerm", ++ "in": "query", ++ "description": "The search term.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "excludeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional filter by items that are marked as favorite, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional, the max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "nameStartsWithOrGreater", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameStartsWith", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameLessThan", ++ "in": "query", ++ "description": "Optional filter by items whose name is equally or lesser than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Sort Order - Ascending,Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional, include image information in output.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Optional. Include total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Music genres returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/MusicGenres/{genreName}": { ++ "get": { ++ "tags": [ ++ "MusicGenres" ++ ], ++ "summary": "Gets a music genre, by name.", ++ "operationId": "GetMusicGenre", ++ "parameters": [ ++ { ++ "name": "genreName", ++ "in": "path", ++ "description": "The genre name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Packages": { ++ "get": { ++ "tags": [ ++ "Package" ++ ], ++ "summary": "Gets available packages.", ++ "operationId": "GetPackages", ++ "responses": { ++ "200": { ++ "description": "Available packages returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PackageInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PackageInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PackageInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Packages/{name}": { ++ "get": { ++ "tags": [ ++ "Package" ++ ], ++ "summary": "Gets a package by name or assembly GUID.", ++ "operationId": "GetPackageInfo", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "The name of the package.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "assemblyGuid", ++ "in": "query", ++ "description": "The GUID of the associated assembly.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Package retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/PackageInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PackageInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PackageInfo" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Packages/Installed/{name}": { ++ "post": { ++ "tags": [ ++ "Package" ++ ], ++ "summary": "Installs a package.", ++ "operationId": "InstallPackage", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Package name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "assemblyGuid", ++ "in": "query", ++ "description": "GUID of the associated assembly.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "version", ++ "in": "query", ++ "description": "Optional version. Defaults to latest version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "repositoryUrl", ++ "in": "query", ++ "description": "Optional. Specify the repository to install from.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Package found." ++ }, ++ "404": { ++ "description": "Package not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Packages/Installing/{packageId}": { ++ "delete": { ++ "tags": [ ++ "Package" ++ ], ++ "summary": "Cancels a package installation.", ++ "operationId": "CancelPackageInstallation", ++ "parameters": [ ++ { ++ "name": "packageId", ++ "in": "path", ++ "description": "Installation Id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Installation cancelled." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Repositories": { ++ "get": { ++ "tags": [ ++ "Package" ++ ], ++ "summary": "Gets all package repositories.", ++ "operationId": "GetRepositories", ++ "responses": { ++ "200": { ++ "description": "Package repositories returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RepositoryInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RepositoryInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RepositoryInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Package" ++ ], ++ "summary": "Sets the enabled and existing package repositories.", ++ "operationId": "SetRepositories", ++ "requestBody": { ++ "description": "The list of package repositories.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RepositoryInfo" ++ } ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RepositoryInfo" ++ } ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RepositoryInfo" ++ } ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Package repositories saved." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Persons": { ++ "get": { ++ "tags": [ ++ "Persons" ++ ], ++ "summary": "Gets all persons.", ++ "operationId": "GetPersons", ++ "parameters": [ ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "searchTerm", ++ "in": "query", ++ "description": "The search term.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "filters", ++ "in": "query", ++ "description": "Optional. Specify additional filters to apply.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFilter" ++ } ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional filter by items that are marked as favorite, or not. userId is required.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional, include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional, the max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "excludePersonTypes", ++ "in": "query", ++ "description": "Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "personTypes", ++ "in": "query", ++ "description": "Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "appearsInItemId", ++ "in": "query", ++ "description": "Optional. If specified, person results will be filtered on items related to said persons.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional, include image information in output.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Persons returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Persons/{name}": { ++ "get": { ++ "tags": [ ++ "Persons" ++ ], ++ "summary": "Get person by name.", ++ "operationId": "GetPerson", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Person name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Person returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Person not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Playlists": { ++ "post": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Creates a new playlist.", ++ "description": "For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.\r\nQuery parameters are obsolete.", ++ "operationId": "CreatePlaylist", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "query", ++ "description": "The playlist name.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "ids", ++ "in": "query", ++ "description": "The item ids.", ++ "deprecated": true, ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "deprecated": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "mediaType", ++ "in": "query", ++ "description": "The media type.", ++ "deprecated": true, ++ "schema": { ++ "enum": [ ++ "Unknown", ++ "Video", ++ "Audio", ++ "Photo", ++ "Book" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ ] ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The create playlist payload.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CreatePlaylistDto" ++ } ++ ], ++ "description": "Create new playlist dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CreatePlaylistDto" ++ } ++ ], ++ "description": "Create new playlist dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CreatePlaylistDto" ++ } ++ ], ++ "description": "Create new playlist dto." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "200": { ++ "description": "Playlist created.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaylistCreationResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaylistCreationResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaylistCreationResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Playlists/{playlistId}": { ++ "post": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Updates a playlist.", ++ "operationId": "UpdatePlaylist", ++ "parameters": [ ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The Jellyfin.Api.Models.PlaylistDtos.UpdatePlaylistDto id.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdatePlaylistDto" ++ } ++ ], ++ "description": "Update existing playlist dto. Fields set to `null` will not be updated and keep their current values." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdatePlaylistDto" ++ } ++ ], ++ "description": "Update existing playlist dto. Fields set to `null` will not be updated and keep their current values." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdatePlaylistDto" ++ } ++ ], ++ "description": "Update existing playlist dto. Fields set to `null` will not be updated and keep their current values." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Playlist updated." ++ }, ++ "403": { ++ "description": "Access forbidden.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Playlist not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "get": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Get a playlist.", ++ "operationId": "GetPlaylist", ++ "parameters": [ ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "The playlist.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaylistDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaylistDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaylistDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Playlist not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Playlists/{playlistId}/Items": { ++ "post": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Adds items to a playlist.", ++ "operationId": "AddItemToPlaylist", ++ "parameters": [ ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "ids", ++ "in": "query", ++ "description": "Item id, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The userId.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Items added to playlist." ++ }, ++ "403": { ++ "description": "Access forbidden.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Playlist not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Removes items from a playlist.", ++ "operationId": "RemoveItemFromPlaylist", ++ "parameters": [ ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "entryIds", ++ "in": "query", ++ "description": "The item ids, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Items removed." ++ }, ++ "403": { ++ "description": "Access forbidden.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Playlist not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "get": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Gets the original items of a playlist.", ++ "operationId": "GetPlaylistItems", ++ "parameters": [ ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Original playlist returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "Forbidden", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Playlist not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Playlists/{playlistId}/Items/{itemId}/Move/{newIndex}": { ++ "post": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Moves a playlist item.", ++ "operationId": "MoveItem", ++ "parameters": [ ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "newIndex", ++ "in": "path", ++ "description": "The new index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Item moved to new index." ++ }, ++ "403": { ++ "description": "Access forbidden.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Playlist not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Playlists/{playlistId}/Users": { ++ "get": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Get a playlist's users.", ++ "operationId": "GetPlaylistUsers", ++ "parameters": [ ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Found shares.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PlaylistUserPermissions" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PlaylistUserPermissions" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PlaylistUserPermissions" ++ } ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "Access forbidden.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Playlist not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Playlists/{playlistId}/Users/{userId}": { ++ "get": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Get a playlist user.", ++ "operationId": "GetPlaylistUser", ++ "parameters": [ ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "path", ++ "description": "The user id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "User permission found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaylistUserPermissions" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaylistUserPermissions" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PlaylistUserPermissions" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "Access forbidden.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Playlist not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Modify a user of a playlist's users.", ++ "operationId": "UpdatePlaylistUser", ++ "parameters": [ ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "path", ++ "description": "The user id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The Jellyfin.Api.Models.PlaylistDtos.UpdatePlaylistUserDto.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdatePlaylistUserDto" ++ } ++ ], ++ "description": "Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdatePlaylistUserDto" ++ } ++ ], ++ "description": "Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdatePlaylistUserDto" ++ } ++ ], ++ "description": "Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "User's permissions modified." ++ }, ++ "403": { ++ "description": "Access forbidden.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Playlist not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Playlists" ++ ], ++ "summary": "Remove a user from a playlist's users.", ++ "operationId": "RemoveUserFromPlaylist", ++ "parameters": [ ++ { ++ "name": "playlistId", ++ "in": "path", ++ "description": "The playlist id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "path", ++ "description": "The user id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "User permissions removed from playlist." ++ }, ++ "403": { ++ "description": "Forbidden", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "No playlist or user permissions found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized access." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/PlayingItems/{itemId}": { ++ "post": { ++ "tags": [ ++ "Playstate" ++ ], ++ "summary": "Reports that a session has begun playing an item.", ++ "operationId": "OnPlaybackStart", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The id of the MediaSource.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "The audio stream index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "The subtitle stream index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "playMethod", ++ "in": "query", ++ "description": "The play method.", ++ "schema": { ++ "enum": [ ++ "Transcode", ++ "DirectStream", ++ "DirectPlay" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "canSeek", ++ "in": "query", ++ "description": "Indicates if the client can seek.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Play start recorded." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Playstate" ++ ], ++ "summary": "Reports that a session has stopped playing an item.", ++ "operationId": "OnPlaybackStopped", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The id of the MediaSource.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nextMediaType", ++ "in": "query", ++ "description": "The next media type that will play.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "positionTicks", ++ "in": "query", ++ "description": "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Playback stop recorded." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/PlayingItems/{itemId}/Progress": { ++ "post": { ++ "tags": [ ++ "Playstate" ++ ], ++ "summary": "Reports a session's playback progress.", ++ "operationId": "OnPlaybackProgress", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The id of the MediaSource.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "positionTicks", ++ "in": "query", ++ "description": "Optional. The current position, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "The audio stream index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "The subtitle stream index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "volumeLevel", ++ "in": "query", ++ "description": "Scale of 0-100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "playMethod", ++ "in": "query", ++ "description": "The play method.", ++ "schema": { ++ "enum": [ ++ "Transcode", ++ "DirectStream", ++ "DirectPlay" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "repeatMode", ++ "in": "query", ++ "description": "The repeat mode.", ++ "schema": { ++ "enum": [ ++ "RepeatNone", ++ "RepeatAll", ++ "RepeatOne" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RepeatMode" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "isPaused", ++ "in": "query", ++ "description": "Indicates if the player is paused.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "isMuted", ++ "in": "query", ++ "description": "Indicates if the player is muted.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Play progress recorded." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/Playing": { ++ "post": { ++ "tags": [ ++ "Playstate" ++ ], ++ "summary": "Reports playback has started within a session.", ++ "operationId": "ReportPlaybackStart", ++ "requestBody": { ++ "description": "The playback start info.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackStartInfo" ++ } ++ ], ++ "description": "Class PlaybackStartInfo." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackStartInfo" ++ } ++ ], ++ "description": "Class PlaybackStartInfo." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackStartInfo" ++ } ++ ], ++ "description": "Class PlaybackStartInfo." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Playback start recorded." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/Playing/Ping": { ++ "post": { ++ "tags": [ ++ "Playstate" ++ ], ++ "summary": "Pings a playback session.", ++ "operationId": "PingPlaybackSession", ++ "parameters": [ ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "Playback session id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Playback session pinged." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/Playing/Progress": { ++ "post": { ++ "tags": [ ++ "Playstate" ++ ], ++ "summary": "Reports playback progress within a session.", ++ "operationId": "ReportPlaybackProgress", ++ "requestBody": { ++ "description": "The playback progress info.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackProgressInfo" ++ } ++ ], ++ "description": "Class PlaybackProgressInfo." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackProgressInfo" ++ } ++ ], ++ "description": "Class PlaybackProgressInfo." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackProgressInfo" ++ } ++ ], ++ "description": "Class PlaybackProgressInfo." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Playback progress recorded." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/Playing/Stopped": { ++ "post": { ++ "tags": [ ++ "Playstate" ++ ], ++ "summary": "Reports playback has stopped within a session.", ++ "operationId": "ReportPlaybackStopped", ++ "requestBody": { ++ "description": "The playback stop info.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackStopInfo" ++ } ++ ], ++ "description": "Class PlaybackStopInfo." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackStopInfo" ++ } ++ ], ++ "description": "Class PlaybackStopInfo." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackStopInfo" ++ } ++ ], ++ "description": "Class PlaybackStopInfo." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Playback stop recorded." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/UserPlayedItems/{itemId}": { ++ "post": { ++ "tags": [ ++ "Playstate" ++ ], ++ "summary": "Marks an item as played for user.", ++ "operationId": "MarkPlayedItem", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "datePlayed", ++ "in": "query", ++ "description": "Optional. The date the item was played.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Item marked as played.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Playstate" ++ ], ++ "summary": "Marks an item as unplayed for user.", ++ "operationId": "MarkUnplayedItem", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Item marked as unplayed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Plugins": { ++ "get": { ++ "tags": [ ++ "Plugins" ++ ], ++ "summary": "Gets a list of currently installed plugins.", ++ "operationId": "GetPlugins", ++ "responses": { ++ "200": { ++ "description": "Installed plugins returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PluginInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PluginInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PluginInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Plugins/{pluginId}": { ++ "delete": { ++ "tags": [ ++ "Plugins" ++ ], ++ "summary": "Uninstalls a plugin.", ++ "operationId": "UninstallPlugin", ++ "parameters": [ ++ { ++ "name": "pluginId", ++ "in": "path", ++ "description": "Plugin id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Plugin uninstalled." ++ }, ++ "404": { ++ "description": "Plugin not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "deprecated": true, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Plugins/{pluginId}/{version}": { ++ "delete": { ++ "tags": [ ++ "Plugins" ++ ], ++ "summary": "Uninstalls a plugin by version.", ++ "operationId": "UninstallPluginByVersion", ++ "parameters": [ ++ { ++ "name": "pluginId", ++ "in": "path", ++ "description": "Plugin id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "version", ++ "in": "path", ++ "description": "Plugin version.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Plugin uninstalled." ++ }, ++ "404": { ++ "description": "Plugin not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Plugins/{pluginId}/{version}/Disable": { ++ "post": { ++ "tags": [ ++ "Plugins" ++ ], ++ "summary": "Disable a plugin.", ++ "operationId": "DisablePlugin", ++ "parameters": [ ++ { ++ "name": "pluginId", ++ "in": "path", ++ "description": "Plugin id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "version", ++ "in": "path", ++ "description": "Plugin version.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Plugin disabled." ++ }, ++ "404": { ++ "description": "Plugin not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Plugins/{pluginId}/{version}/Enable": { ++ "post": { ++ "tags": [ ++ "Plugins" ++ ], ++ "summary": "Enables a disabled plugin.", ++ "operationId": "EnablePlugin", ++ "parameters": [ ++ { ++ "name": "pluginId", ++ "in": "path", ++ "description": "Plugin id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "version", ++ "in": "path", ++ "description": "Plugin version.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Plugin enabled." ++ }, ++ "404": { ++ "description": "Plugin not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Plugins/{pluginId}/{version}/Image": { ++ "get": { ++ "tags": [ ++ "Plugins" ++ ], ++ "summary": "Gets a plugin's image.", ++ "operationId": "GetPluginImage", ++ "parameters": [ ++ { ++ "name": "pluginId", ++ "in": "path", ++ "description": "Plugin id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "version", ++ "in": "path", ++ "description": "Plugin version.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Plugin image returned.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Not Found", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Plugins/{pluginId}/Configuration": { ++ "get": { ++ "tags": [ ++ "Plugins" ++ ], ++ "summary": "Gets plugin configuration.", ++ "operationId": "GetPluginConfiguration", ++ "parameters": [ ++ { ++ "name": "pluginId", ++ "in": "path", ++ "description": "Plugin id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Plugin configuration returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BasePluginConfiguration" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BasePluginConfiguration" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BasePluginConfiguration" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Plugin not found or plugin configuration not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Plugins" ++ ], ++ "summary": "Updates plugin configuration.", ++ "description": "Accepts plugin configuration as JSON body.", ++ "operationId": "UpdatePluginConfiguration", ++ "parameters": [ ++ { ++ "name": "pluginId", ++ "in": "path", ++ "description": "Plugin id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Plugin configuration updated." ++ }, ++ "404": { ++ "description": "Plugin not found or plugin does not have configuration.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Plugins/{pluginId}/Manifest": { ++ "post": { ++ "tags": [ ++ "Plugins" ++ ], ++ "summary": "Gets a plugin's manifest.", ++ "operationId": "GetPluginManifest", ++ "parameters": [ ++ { ++ "name": "pluginId", ++ "in": "path", ++ "description": "Plugin id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Plugin manifest returned." ++ }, ++ "404": { ++ "description": "Plugin not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/QuickConnect/Authorize": { ++ "post": { ++ "tags": [ ++ "QuickConnect" ++ ], ++ "summary": "Authorizes a pending quick connect request.", ++ "operationId": "AuthorizeQuickConnect", ++ "parameters": [ ++ { ++ "name": "code", ++ "in": "query", ++ "description": "Quick connect code to authorize.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user the authorize. Access to the requested user is required.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Quick connect result authorized successfully.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "Unknown user id.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/QuickConnect/Connect": { ++ "get": { ++ "tags": [ ++ "QuickConnect" ++ ], ++ "summary": "Attempts to retrieve authentication information.", ++ "operationId": "GetQuickConnectState", ++ "parameters": [ ++ { ++ "name": "secret", ++ "in": "query", ++ "description": "Secret previously returned from the Initiate endpoint.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Quick connect result returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/QuickConnectResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/QuickConnectResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/QuickConnectResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Unknown quick connect secret.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/QuickConnect/Enabled": { ++ "get": { ++ "tags": [ ++ "QuickConnect" ++ ], ++ "summary": "Gets the current quick connect state.", ++ "operationId": "GetQuickConnectEnabled", ++ "responses": { ++ "200": { ++ "description": "Quick connect state returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/QuickConnect/Initiate": { ++ "post": { ++ "tags": [ ++ "QuickConnect" ++ ], ++ "summary": "Initiate a new quick connect request.", ++ "operationId": "InitiateQuickConnect", ++ "responses": { ++ "200": { ++ "description": "Quick connect request successfully created.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/QuickConnectResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/QuickConnectResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/QuickConnectResult" ++ } ++ } ++ } ++ }, ++ "401": { ++ "description": "Quick connect is not active on this server." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Items/{itemId}/RemoteImages": { ++ "get": { ++ "tags": [ ++ "RemoteImage" ++ ], ++ "summary": "Gets available remote images for an item.", ++ "operationId": "GetRemoteImages", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item Id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "type", ++ "in": "query", ++ "description": "The image type.", ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "providerName", ++ "in": "query", ++ "description": "Optional. The image provider to use.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "includeAllLanguages", ++ "in": "query", ++ "description": "Optional. Include all languages.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Remote Images returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/RemoteImageResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/RemoteImageResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/RemoteImageResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/RemoteImages/Download": { ++ "post": { ++ "tags": [ ++ "RemoteImage" ++ ], ++ "summary": "Downloads a remote image for an item.", ++ "operationId": "DownloadRemoteImage", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item Id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "type", ++ "in": "query", ++ "description": "The image type.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Enum ImageType." ++ } ++ }, ++ { ++ "name": "imageUrl", ++ "in": "query", ++ "description": "The image url.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Remote image downloaded." ++ }, ++ "404": { ++ "description": "Remote image not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/RemoteImages/Providers": { ++ "get": { ++ "tags": [ ++ "RemoteImage" ++ ], ++ "summary": "Gets available remote image providers for an item.", ++ "operationId": "GetRemoteImageProviders", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item Id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Returned remote image providers.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageProviderInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageProviderInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageProviderInfo" ++ } ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/ScheduledTasks": { ++ "get": { ++ "tags": [ ++ "ScheduledTasks" ++ ], ++ "summary": "Get tasks.", ++ "operationId": "GetTasks", ++ "parameters": [ ++ { ++ "name": "isHidden", ++ "in": "query", ++ "description": "Optional filter tasks that are hidden, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isEnabled", ++ "in": "query", ++ "description": "Optional filter tasks that are enabled, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Scheduled tasks retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TaskInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TaskInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TaskInfo" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/ScheduledTasks/{taskId}": { ++ "get": { ++ "tags": [ ++ "ScheduledTasks" ++ ], ++ "summary": "Get task by id.", ++ "operationId": "GetTask", ++ "parameters": [ ++ { ++ "name": "taskId", ++ "in": "path", ++ "description": "Task Id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Task retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/TaskInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/TaskInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/TaskInfo" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Task not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/ScheduledTasks/{taskId}/Triggers": { ++ "post": { ++ "tags": [ ++ "ScheduledTasks" ++ ], ++ "summary": "Update specified task triggers.", ++ "operationId": "UpdateTask", ++ "parameters": [ ++ { ++ "name": "taskId", ++ "in": "path", ++ "description": "Task Id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "Triggers.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TaskTriggerInfo" ++ } ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TaskTriggerInfo" ++ } ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TaskTriggerInfo" ++ } ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Task triggers updated." ++ }, ++ "404": { ++ "description": "Task not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/ScheduledTasks/Running/{taskId}": { ++ "post": { ++ "tags": [ ++ "ScheduledTasks" ++ ], ++ "summary": "Start specified task.", ++ "operationId": "StartTask", ++ "parameters": [ ++ { ++ "name": "taskId", ++ "in": "path", ++ "description": "Task Id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Task started." ++ }, ++ "404": { ++ "description": "Task not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "ScheduledTasks" ++ ], ++ "summary": "Stop specified task.", ++ "operationId": "StopTask", ++ "parameters": [ ++ { ++ "name": "taskId", ++ "in": "path", ++ "description": "Task Id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Task stopped." ++ }, ++ "404": { ++ "description": "Task not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Search/Hints": { ++ "get": { ++ "tags": [ ++ "Search" ++ ], ++ "summary": "Gets the search hint result.", ++ "operationId": "GetSearchHints", ++ "parameters": [ ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Supply a user id to search within a user's library or omit to search all.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "searchTerm", ++ "in": "query", ++ "description": "The search term to filter on.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "If specified, only results with the specified item types are returned. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "excludeItemTypes", ++ "in": "query", ++ "description": "If specified, results with these item types are filtered out. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "mediaTypes", ++ "in": "query", ++ "description": "If specified, only results with the specified media types are returned. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "If specified, only children of the parent are returned.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "isMovie", ++ "in": "query", ++ "description": "Optional filter for movies.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSeries", ++ "in": "query", ++ "description": "Optional filter for series.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isNews", ++ "in": "query", ++ "description": "Optional filter for news.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isKids", ++ "in": "query", ++ "description": "Optional filter for kids.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSports", ++ "in": "query", ++ "description": "Optional filter for sports.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "includePeople", ++ "in": "query", ++ "description": "Optional filter whether to include people.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "includeMedia", ++ "in": "query", ++ "description": "Optional filter whether to include media.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "includeGenres", ++ "in": "query", ++ "description": "Optional filter whether to include genres.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "includeStudios", ++ "in": "query", ++ "description": "Optional filter whether to include studios.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "includeArtists", ++ "in": "query", ++ "description": "Optional filter whether to include artists.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Search hint returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/SearchHintResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SearchHintResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SearchHintResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Auth/PasswordResetProviders": { ++ "get": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Get all password reset providers.", ++ "operationId": "GetPasswordResetProviders", ++ "responses": { ++ "200": { ++ "description": "Password reset providers retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Auth/Providers": { ++ "get": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Get all auth providers.", ++ "operationId": "GetAuthProviders", ++ "responses": { ++ "200": { ++ "description": "Auth providers retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions": { ++ "get": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Gets a list of sessions.", ++ "operationId": "GetSessions", ++ "parameters": [ ++ { ++ "name": "controllableByUserId", ++ "in": "query", ++ "description": "Filter by sessions that a given user is allowed to remote control.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "Filter by device Id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "activeWithinSeconds", ++ "in": "query", ++ "description": "Optional. Filter by sessions that were active in the last n seconds.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "List of sessions returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SessionInfoDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SessionInfoDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SessionInfoDto" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/{sessionId}/Command": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Issues a full general command to a client.", ++ "operationId": "SendFullGeneralCommand", ++ "parameters": [ ++ { ++ "name": "sessionId", ++ "in": "path", ++ "description": "The session id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The MediaBrowser.Model.Session.GeneralCommand.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GeneralCommand" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GeneralCommand" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GeneralCommand" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Full general command sent to session." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/{sessionId}/Command/{command}": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Issues a general command to a client.", ++ "operationId": "SendGeneralCommand", ++ "parameters": [ ++ { ++ "name": "sessionId", ++ "in": "path", ++ "description": "The session id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "command", ++ "in": "path", ++ "description": "The command to send.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "MoveUp", ++ "MoveDown", ++ "MoveLeft", ++ "MoveRight", ++ "PageUp", ++ "PageDown", ++ "PreviousLetter", ++ "NextLetter", ++ "ToggleOsd", ++ "ToggleContextMenu", ++ "Select", ++ "Back", ++ "TakeScreenshot", ++ "SendKey", ++ "SendString", ++ "GoHome", ++ "GoToSettings", ++ "VolumeUp", ++ "VolumeDown", ++ "Mute", ++ "Unmute", ++ "ToggleMute", ++ "SetVolume", ++ "SetAudioStreamIndex", ++ "SetSubtitleStreamIndex", ++ "ToggleFullscreen", ++ "DisplayContent", ++ "GoToSearch", ++ "DisplayMessage", ++ "SetRepeatMode", ++ "ChannelUp", ++ "ChannelDown", ++ "Guide", ++ "ToggleStats", ++ "PlayMediaSource", ++ "PlayTrailers", ++ "SetShuffleQueue", ++ "PlayState", ++ "PlayNext", ++ "ToggleOsdMenu", ++ "Play", ++ "SetMaxStreamingBitrate", ++ "SetPlaybackOrder" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GeneralCommandType" ++ } ++ ], ++ "description": "This exists simply to identify a set of known commands." ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "General command sent to session." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/{sessionId}/Message": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Issues a command to a client to display a message to the user.", ++ "operationId": "SendMessageCommand", ++ "parameters": [ ++ { ++ "name": "sessionId", ++ "in": "path", ++ "description": "The session id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The MediaBrowser.Model.Session.MessageCommand object containing Header, Message Text, and TimeoutMs.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MessageCommand" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MessageCommand" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MessageCommand" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Message sent." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/{sessionId}/Playing": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Instructs a session to play an item.", ++ "operationId": "Play", ++ "parameters": [ ++ { ++ "name": "sessionId", ++ "in": "path", ++ "description": "The session id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playCommand", ++ "in": "query", ++ "description": "The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "PlayNow", ++ "PlayNext", ++ "PlayLast", ++ "PlayInstantMix", ++ "PlayShuffle" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayCommand" ++ } ++ ], ++ "description": "Enum PlayCommand." ++ } ++ }, ++ { ++ "name": "itemIds", ++ "in": "query", ++ "description": "The ids of the items to play, comma delimited.", ++ "required": true, ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "startPositionTicks", ++ "in": "query", ++ "description": "The starting position of the first item.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "Optional. The media source id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to play.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to play.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The start index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Instruction sent to session." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/{sessionId}/Playing/{command}": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Issues a playstate command to a client.", ++ "operationId": "SendPlaystateCommand", ++ "parameters": [ ++ { ++ "name": "sessionId", ++ "in": "path", ++ "description": "The session id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "command", ++ "in": "path", ++ "description": "The MediaBrowser.Model.Session.PlaystateCommand.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "Stop", ++ "Pause", ++ "Unpause", ++ "NextTrack", ++ "PreviousTrack", ++ "Seek", ++ "Rewind", ++ "FastForward", ++ "PlayPause" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaystateCommand" ++ } ++ ], ++ "description": "Enum PlaystateCommand." ++ } ++ }, ++ { ++ "name": "seekPositionTicks", ++ "in": "query", ++ "description": "The optional position ticks.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "controllingUserId", ++ "in": "query", ++ "description": "The optional controlling user id.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Playstate command sent to session." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/{sessionId}/System/{command}": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Issues a system command to a client.", ++ "operationId": "SendSystemCommand", ++ "parameters": [ ++ { ++ "name": "sessionId", ++ "in": "path", ++ "description": "The session id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "command", ++ "in": "path", ++ "description": "The command to send.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "MoveUp", ++ "MoveDown", ++ "MoveLeft", ++ "MoveRight", ++ "PageUp", ++ "PageDown", ++ "PreviousLetter", ++ "NextLetter", ++ "ToggleOsd", ++ "ToggleContextMenu", ++ "Select", ++ "Back", ++ "TakeScreenshot", ++ "SendKey", ++ "SendString", ++ "GoHome", ++ "GoToSettings", ++ "VolumeUp", ++ "VolumeDown", ++ "Mute", ++ "Unmute", ++ "ToggleMute", ++ "SetVolume", ++ "SetAudioStreamIndex", ++ "SetSubtitleStreamIndex", ++ "ToggleFullscreen", ++ "DisplayContent", ++ "GoToSearch", ++ "DisplayMessage", ++ "SetRepeatMode", ++ "ChannelUp", ++ "ChannelDown", ++ "Guide", ++ "ToggleStats", ++ "PlayMediaSource", ++ "PlayTrailers", ++ "SetShuffleQueue", ++ "PlayState", ++ "PlayNext", ++ "ToggleOsdMenu", ++ "Play", ++ "SetMaxStreamingBitrate", ++ "SetPlaybackOrder" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GeneralCommandType" ++ } ++ ], ++ "description": "This exists simply to identify a set of known commands." ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "System command sent to session." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/{sessionId}/User/{userId}": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Adds an additional user to a session.", ++ "operationId": "AddUserToSession", ++ "parameters": [ ++ { ++ "name": "sessionId", ++ "in": "path", ++ "description": "The session id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "path", ++ "description": "The user id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "User added to session." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Removes an additional user from a session.", ++ "operationId": "RemoveUserFromSession", ++ "parameters": [ ++ { ++ "name": "sessionId", ++ "in": "path", ++ "description": "The session id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "path", ++ "description": "The user id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "User removed from session." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/{sessionId}/Viewing": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Instructs a session to browse to an item or view.", ++ "operationId": "DisplayContent", ++ "parameters": [ ++ { ++ "name": "sessionId", ++ "in": "path", ++ "description": "The session Id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "itemType", ++ "in": "query", ++ "description": "The type of item to browse to.", ++ "required": true, ++ "schema": { ++ "enum": [ ++ "AggregateFolder", ++ "Audio", ++ "AudioBook", ++ "BasePluginFolder", ++ "Book", ++ "BoxSet", ++ "Channel", ++ "ChannelFolderItem", ++ "CollectionFolder", ++ "Episode", ++ "Folder", ++ "Genre", ++ "ManualPlaylistsFolder", ++ "Movie", ++ "LiveTvChannel", ++ "LiveTvProgram", ++ "MusicAlbum", ++ "MusicArtist", ++ "MusicGenre", ++ "MusicVideo", ++ "Person", ++ "Photo", ++ "PhotoAlbum", ++ "Playlist", ++ "PlaylistsFolder", ++ "Program", ++ "Recording", ++ "Season", ++ "Series", ++ "Studio", ++ "Trailer", ++ "TvChannel", ++ "TvProgram", ++ "UserRootFolder", ++ "UserView", ++ "Video", ++ "Year" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ ], ++ "description": "The base item kind." ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "query", ++ "description": "The Id of the item.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "itemName", ++ "in": "query", ++ "description": "The name of the item.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Instruction sent to session." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/Capabilities": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Updates capabilities for a device.", ++ "operationId": "PostCapabilities", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "The session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playableMediaTypes", ++ "in": "query", ++ "description": "A list of playable media types, comma delimited. Audio, Video, Book, Photo.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ } ++ }, ++ { ++ "name": "supportedCommands", ++ "in": "query", ++ "description": "A list of supported remote control commands, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/GeneralCommandType" ++ } ++ } ++ }, ++ { ++ "name": "supportsMediaControl", ++ "in": "query", ++ "description": "Determines whether media can be played remotely..", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "supportsPersistentIdentifier", ++ "in": "query", ++ "description": "Determines whether the device supports a unique identifier.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Capabilities posted." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/Capabilities/Full": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Updates capabilities for a device.", ++ "operationId": "PostFullCapabilities", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "query", ++ "description": "The session id.", ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The MediaBrowser.Model.Session.ClientCapabilities.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ClientCapabilitiesDto" ++ } ++ ], ++ "description": "Client capabilities dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ClientCapabilitiesDto" ++ } ++ ], ++ "description": "Client capabilities dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ClientCapabilitiesDto" ++ } ++ ], ++ "description": "Client capabilities dto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Capabilities updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/Logout": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Reports that a session has ended.", ++ "operationId": "ReportSessionEnded", ++ "responses": { ++ "204": { ++ "description": "Session end reported to server." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Sessions/Viewing": { ++ "post": { ++ "tags": [ ++ "Session" ++ ], ++ "summary": "Reports that a session is viewing an item.", ++ "operationId": "ReportViewing", ++ "parameters": [ ++ { ++ "name": "sessionId", ++ "in": "query", ++ "description": "The session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "query", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Session reported to server." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Startup/Complete": { ++ "post": { ++ "tags": [ ++ "Startup" ++ ], ++ "summary": "Completes the startup wizard.", ++ "operationId": "CompleteWizard", ++ "responses": { ++ "204": { ++ "description": "Startup wizard completed." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Startup/Configuration": { ++ "get": { ++ "tags": [ ++ "Startup" ++ ], ++ "summary": "Gets the initial startup wizard configuration.", ++ "operationId": "GetStartupConfiguration", ++ "responses": { ++ "200": { ++ "description": "Initial startup wizard configuration retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/StartupConfigurationDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/StartupConfigurationDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/StartupConfigurationDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Startup" ++ ], ++ "summary": "Sets the initial startup wizard configuration.", ++ "operationId": "UpdateInitialConfiguration", ++ "requestBody": { ++ "description": "The updated startup configuration.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/StartupConfigurationDto" ++ } ++ ], ++ "description": "The startup configuration DTO." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/StartupConfigurationDto" ++ } ++ ], ++ "description": "The startup configuration DTO." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/StartupConfigurationDto" ++ } ++ ], ++ "description": "The startup configuration DTO." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Configuration saved." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Startup/FirstUser": { ++ "get": { ++ "tags": [ ++ "Startup" ++ ], ++ "summary": "Gets the first user.", ++ "operationId": "GetFirstUser_2", ++ "responses": { ++ "200": { ++ "description": "Initial user retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/StartupUserDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/StartupUserDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/StartupUserDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Startup/RemoteAccess": { ++ "post": { ++ "tags": [ ++ "Startup" ++ ], ++ "summary": "Sets remote access and UPnP.", ++ "operationId": "SetRemoteAccess", ++ "requestBody": { ++ "description": "The startup remote access dto.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/StartupRemoteAccessDto" ++ } ++ ], ++ "description": "Startup remote access dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/StartupRemoteAccessDto" ++ } ++ ], ++ "description": "Startup remote access dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/StartupRemoteAccessDto" ++ } ++ ], ++ "description": "Startup remote access dto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Configuration saved." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Startup/User": { ++ "get": { ++ "tags": [ ++ "Startup" ++ ], ++ "summary": "Gets the first user.", ++ "operationId": "GetFirstUser", ++ "responses": { ++ "200": { ++ "description": "Initial user retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/StartupUserDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/StartupUserDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/StartupUserDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "Startup" ++ ], ++ "summary": "Sets the user name and password.", ++ "operationId": "UpdateStartupUser", ++ "requestBody": { ++ "description": "The DTO containing username and password.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/StartupUserDto" ++ } ++ ], ++ "description": "The startup user DTO." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/StartupUserDto" ++ } ++ ], ++ "description": "The startup user DTO." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/StartupUserDto" ++ } ++ ], ++ "description": "The startup user DTO." ++ } ++ } ++ } ++ }, ++ "responses": { ++ "204": { ++ "description": "Updated user name and password." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrElevated", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Studios": { ++ "get": { ++ "tags": [ ++ "Studios" ++ ], ++ "summary": "Gets all studios from a given item, folder, or the entire library.", ++ "operationId": "GetStudios", ++ "parameters": [ ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "searchTerm", ++ "in": "query", ++ "description": "Optional. Search term.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "excludeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional filter by items that are marked as favorite, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional, include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional, the max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "nameStartsWithOrGreater", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameStartsWith", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameLessThan", ++ "in": "query", ++ "description": "Optional filter by items whose name is equally or lesser than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional, include image information in output.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Studios returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Studios/{name}": { ++ "get": { ++ "tags": [ ++ "Studios" ++ ], ++ "summary": "Gets a studio by name.", ++ "operationId": "GetStudio", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "Studio name.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Studio returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/FallbackFont/Fonts": { ++ "get": { ++ "tags": [ ++ "Subtitle" ++ ], ++ "summary": "Gets a list of available fallback font files.", ++ "operationId": "GetFallbackFontList", ++ "responses": { ++ "200": { ++ "description": "Information retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FontFile" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FontFile" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FontFile" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/FallbackFont/Fonts/{name}": { ++ "get": { ++ "tags": [ ++ "Subtitle" ++ ], ++ "summary": "Gets a fallback font file.", ++ "operationId": "GetFallbackFont", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "path", ++ "description": "The name of the fallback font file to get.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Fallback font file retrieved.", ++ "content": { ++ "font/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/RemoteSearch/Subtitles/{language}": { ++ "get": { ++ "tags": [ ++ "Subtitle" ++ ], ++ "summary": "Search remote subtitles.", ++ "operationId": "SearchRemoteSubtitles", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "language", ++ "in": "path", ++ "description": "The language of the subtitles.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "isPerfectMatch", ++ "in": "query", ++ "description": "Optional. Only show subtitles which are a perfect match.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Subtitles retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSubtitleInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSubtitleInfo" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSubtitleInfo" ++ } ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SubtitleManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}": { ++ "post": { ++ "tags": [ ++ "Subtitle" ++ ], ++ "summary": "Downloads a remote subtitle.", ++ "operationId": "DownloadRemoteSubtitles", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "subtitleId", ++ "in": "path", ++ "description": "The subtitle id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Subtitle downloaded." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SubtitleManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Providers/Subtitles/Subtitles/{subtitleId}": { ++ "get": { ++ "tags": [ ++ "Subtitle" ++ ], ++ "summary": "Gets the remote subtitles.", ++ "operationId": "GetRemoteSubtitles", ++ "parameters": [ ++ { ++ "name": "subtitleId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "File returned.", ++ "content": { ++ "text/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SubtitleManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8": { ++ "get": { ++ "tags": [ ++ "Subtitle" ++ ], ++ "summary": "Gets an HLS subtitle playlist.", ++ "operationId": "GetSubtitlePlaylist", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "index", ++ "in": "path", ++ "description": "The subtitle stream index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "path", ++ "description": "The media source id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The subtitle segment length.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Subtitle playlist retrieved.", ++ "content": { ++ "application/x-mpegURL": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/Subtitles": { ++ "post": { ++ "tags": [ ++ "Subtitle" ++ ], ++ "summary": "Upload an external subtitle file.", ++ "operationId": "UploadSubtitle", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item the subtitle belongs to.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The request body.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UploadSubtitleDto" ++ } ++ ], ++ "description": "Upload subtitles dto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UploadSubtitleDto" ++ } ++ ], ++ "description": "Upload subtitles dto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UploadSubtitleDto" ++ } ++ ], ++ "description": "Upload subtitles dto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Subtitle uploaded." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SubtitleManagement", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/Subtitles/{index}": { ++ "delete": { ++ "tags": [ ++ "Subtitle" ++ ], ++ "summary": "Deletes an external subtitle file.", ++ "operationId": "DeleteSubtitle", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "index", ++ "in": "path", ++ "description": "The index of the subtitle file.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Subtitle deleted." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}": { ++ "get": { ++ "tags": [ ++ "Subtitle" ++ ], ++ "summary": "Gets subtitles in a specified format.", ++ "operationId": "GetSubtitleWithTicks", ++ "parameters": [ ++ { ++ "name": "routeItemId", ++ "in": "path", ++ "description": "The (route) item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "routeMediaSourceId", ++ "in": "path", ++ "description": "The (route) media source id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "routeIndex", ++ "in": "path", ++ "description": "The (route) subtitle stream index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "routeStartPositionTicks", ++ "in": "path", ++ "description": "The (route) start position of the subtitle in ticks.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "routeFormat", ++ "in": "path", ++ "description": "The (route) format of the returned subtitle.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "query", ++ "description": "The item id.", ++ "deprecated": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media source id.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "index", ++ "in": "query", ++ "description": "The subtitle stream index.", ++ "deprecated": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "startPositionTicks", ++ "in": "query", ++ "description": "The start position of the subtitle in ticks.", ++ "deprecated": true, ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "The format of the returned subtitle.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "endPositionTicks", ++ "in": "query", ++ "description": "Optional. The end position of the subtitle in ticks.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Optional. Whether to copy the timestamps.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "addVttTimeMap", ++ "in": "query", ++ "description": "Optional. Whether to add a VTT time map.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "File returned.", ++ "content": { ++ "text/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}": { ++ "get": { ++ "tags": [ ++ "Subtitle" ++ ], ++ "summary": "Gets subtitles in a specified format.", ++ "operationId": "GetSubtitle", ++ "parameters": [ ++ { ++ "name": "routeItemId", ++ "in": "path", ++ "description": "The (route) item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "routeMediaSourceId", ++ "in": "path", ++ "description": "The (route) media source id.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "routeIndex", ++ "in": "path", ++ "description": "The (route) subtitle stream index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "routeFormat", ++ "in": "path", ++ "description": "The (route) format of the returned subtitle.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "query", ++ "description": "The item id.", ++ "deprecated": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media source id.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "index", ++ "in": "query", ++ "description": "The subtitle stream index.", ++ "deprecated": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "format", ++ "in": "query", ++ "description": "The format of the returned subtitle.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "endPositionTicks", ++ "in": "query", ++ "description": "Optional. The end position of the subtitle in ticks.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Optional. Whether to copy the timestamps.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "addVttTimeMap", ++ "in": "query", ++ "description": "Optional. Whether to add a VTT time map.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "startPositionTicks", ++ "in": "query", ++ "description": "The start position of the subtitle in ticks.", ++ "schema": { ++ "type": "integer", ++ "format": "int64", ++ "default": 0 ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "File returned.", ++ "content": { ++ "text/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Items/Suggestions": { ++ "get": { ++ "tags": [ ++ "Suggestions" ++ ], ++ "summary": "Gets suggestions.", ++ "operationId": "GetSuggestions", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "mediaType", ++ "in": "query", ++ "description": "The media types.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ } ++ }, ++ { ++ "name": "type", ++ "in": "query", ++ "description": "The type.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The start index.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The limit.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Whether to enable the total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Suggestions returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/{id}": { ++ "get": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Gets a SyncPlay group by id.", ++ "operationId": "SyncPlayGetGroup", ++ "parameters": [ ++ { ++ "name": "id", ++ "in": "path", ++ "description": "The id of the group.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Group returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/GroupInfoDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/GroupInfoDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/GroupInfoDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Not Found", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayJoinGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/Buffering": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Notify SyncPlay group that member is buffering.", ++ "operationId": "SyncPlayBuffering", ++ "requestBody": { ++ "description": "The player status.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BufferRequestDto" ++ } ++ ], ++ "description": "Class BufferRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BufferRequestDto" ++ } ++ ], ++ "description": "Class BufferRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BufferRequestDto" ++ } ++ ], ++ "description": "Class BufferRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Group state update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/Join": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Join an existing SyncPlay group.", ++ "operationId": "SyncPlayJoinGroup", ++ "requestBody": { ++ "description": "The group to join.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/JoinGroupRequestDto" ++ } ++ ], ++ "description": "Class JoinGroupRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/JoinGroupRequestDto" ++ } ++ ], ++ "description": "Class JoinGroupRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/JoinGroupRequestDto" ++ } ++ ], ++ "description": "Class JoinGroupRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Group join successful." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayJoinGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/Leave": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Leave the joined SyncPlay group.", ++ "operationId": "SyncPlayLeaveGroup", ++ "responses": { ++ "204": { ++ "description": "Group leave successful." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/List": { ++ "get": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Gets all SyncPlay groups.", ++ "operationId": "SyncPlayGetGroups", ++ "responses": { ++ "200": { ++ "description": "Groups returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/GroupInfoDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/GroupInfoDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/GroupInfoDto" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayJoinGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/MovePlaylistItem": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request to move an item in the playlist in SyncPlay group.", ++ "operationId": "SyncPlayMovePlaylistItem", ++ "requestBody": { ++ "description": "The new position for the item.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MovePlaylistItemRequestDto" ++ } ++ ], ++ "description": "Class MovePlaylistItemRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MovePlaylistItemRequestDto" ++ } ++ ], ++ "description": "Class MovePlaylistItemRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MovePlaylistItemRequestDto" ++ } ++ ], ++ "description": "Class MovePlaylistItemRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Queue update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/New": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Create a new SyncPlay group.", ++ "operationId": "SyncPlayCreateGroup", ++ "requestBody": { ++ "description": "The settings of the new group.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/NewGroupRequestDto" ++ } ++ ], ++ "description": "Class NewGroupRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/NewGroupRequestDto" ++ } ++ ], ++ "description": "Class NewGroupRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/NewGroupRequestDto" ++ } ++ ], ++ "description": "Class NewGroupRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/GroupInfoDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/GroupInfoDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/GroupInfoDto" ++ } ++ } ++ } ++ }, ++ "204": { ++ "description": "New group created." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayCreateGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/NextItem": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request next item in SyncPlay group.", ++ "operationId": "SyncPlayNextItem", ++ "requestBody": { ++ "description": "The current item information.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/NextItemRequestDto" ++ } ++ ], ++ "description": "Class NextItemRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/NextItemRequestDto" ++ } ++ ], ++ "description": "Class NextItemRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/NextItemRequestDto" ++ } ++ ], ++ "description": "Class NextItemRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Next item update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/Pause": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request pause in SyncPlay group.", ++ "operationId": "SyncPlayPause", ++ "responses": { ++ "204": { ++ "description": "Pause update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/Ping": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Update session ping.", ++ "operationId": "SyncPlayPing", ++ "requestBody": { ++ "description": "The new ping.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PingRequestDto" ++ } ++ ], ++ "description": "Class PingRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PingRequestDto" ++ } ++ ], ++ "description": "Class PingRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PingRequestDto" ++ } ++ ], ++ "description": "Class PingRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Ping updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/PreviousItem": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request previous item in SyncPlay group.", ++ "operationId": "SyncPlayPreviousItem", ++ "requestBody": { ++ "description": "The current item information.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PreviousItemRequestDto" ++ } ++ ], ++ "description": "Class PreviousItemRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PreviousItemRequestDto" ++ } ++ ], ++ "description": "Class PreviousItemRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PreviousItemRequestDto" ++ } ++ ], ++ "description": "Class PreviousItemRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Previous item update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/Queue": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request to queue items to the playlist of a SyncPlay group.", ++ "operationId": "SyncPlayQueue", ++ "requestBody": { ++ "description": "The items to add.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/QueueRequestDto" ++ } ++ ], ++ "description": "Class QueueRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/QueueRequestDto" ++ } ++ ], ++ "description": "Class QueueRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/QueueRequestDto" ++ } ++ ], ++ "description": "Class QueueRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Queue update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/Ready": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Notify SyncPlay group that member is ready for playback.", ++ "operationId": "SyncPlayReady", ++ "requestBody": { ++ "description": "The player status.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ReadyRequestDto" ++ } ++ ], ++ "description": "Class ReadyRequest." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ReadyRequestDto" ++ } ++ ], ++ "description": "Class ReadyRequest." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ReadyRequestDto" ++ } ++ ], ++ "description": "Class ReadyRequest." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Group state update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/RemoveFromPlaylist": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request to remove items from the playlist in SyncPlay group.", ++ "operationId": "SyncPlayRemoveFromPlaylist", ++ "requestBody": { ++ "description": "The items to remove.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RemoveFromPlaylistRequestDto" ++ } ++ ], ++ "description": "Class RemoveFromPlaylistRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RemoveFromPlaylistRequestDto" ++ } ++ ], ++ "description": "Class RemoveFromPlaylistRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RemoveFromPlaylistRequestDto" ++ } ++ ], ++ "description": "Class RemoveFromPlaylistRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Queue update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/Seek": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request seek in SyncPlay group.", ++ "operationId": "SyncPlaySeek", ++ "requestBody": { ++ "description": "The new playback position.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeekRequestDto" ++ } ++ ], ++ "description": "Class SeekRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeekRequestDto" ++ } ++ ], ++ "description": "Class SeekRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeekRequestDto" ++ } ++ ], ++ "description": "Class SeekRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Seek update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/SetIgnoreWait": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request SyncPlay group to ignore member during group-wait.", ++ "operationId": "SyncPlaySetIgnoreWait", ++ "requestBody": { ++ "description": "The settings to set.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/IgnoreWaitRequestDto" ++ } ++ ], ++ "description": "Class IgnoreWaitRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/IgnoreWaitRequestDto" ++ } ++ ], ++ "description": "Class IgnoreWaitRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/IgnoreWaitRequestDto" ++ } ++ ], ++ "description": "Class IgnoreWaitRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Member state updated." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/SetNewQueue": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request to set new playlist in SyncPlay group.", ++ "operationId": "SyncPlaySetNewQueue", ++ "requestBody": { ++ "description": "The new playlist to play in the group.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayRequestDto" ++ } ++ ], ++ "description": "Class PlayRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayRequestDto" ++ } ++ ], ++ "description": "Class PlayRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayRequestDto" ++ } ++ ], ++ "description": "Class PlayRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Queue update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/SetPlaylistItem": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request to change playlist item in SyncPlay group.", ++ "operationId": "SyncPlaySetPlaylistItem", ++ "requestBody": { ++ "description": "The new item to play.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetPlaylistItemRequestDto" ++ } ++ ], ++ "description": "Class SetPlaylistItemRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetPlaylistItemRequestDto" ++ } ++ ], ++ "description": "Class SetPlaylistItemRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetPlaylistItemRequestDto" ++ } ++ ], ++ "description": "Class SetPlaylistItemRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Queue update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/SetRepeatMode": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request to set repeat mode in SyncPlay group.", ++ "operationId": "SyncPlaySetRepeatMode", ++ "requestBody": { ++ "description": "The new repeat mode.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetRepeatModeRequestDto" ++ } ++ ], ++ "description": "Class SetRepeatModeRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetRepeatModeRequestDto" ++ } ++ ], ++ "description": "Class SetRepeatModeRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetRepeatModeRequestDto" ++ } ++ ], ++ "description": "Class SetRepeatModeRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Play queue update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/SetShuffleMode": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request to set shuffle mode in SyncPlay group.", ++ "operationId": "SyncPlaySetShuffleMode", ++ "requestBody": { ++ "description": "The new shuffle mode.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetShuffleModeRequestDto" ++ } ++ ], ++ "description": "Class SetShuffleModeRequestDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetShuffleModeRequestDto" ++ } ++ ], ++ "description": "Class SetShuffleModeRequestDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SetShuffleModeRequestDto" ++ } ++ ], ++ "description": "Class SetShuffleModeRequestDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Play queue update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/Stop": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request stop in SyncPlay group.", ++ "operationId": "SyncPlayStop", ++ "responses": { ++ "204": { ++ "description": "Stop update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/SyncPlay/Unpause": { ++ "post": { ++ "tags": [ ++ "SyncPlay" ++ ], ++ "summary": "Request unpause in SyncPlay group.", ++ "operationId": "SyncPlayUnpause", ++ "responses": { ++ "204": { ++ "description": "Unpause update sent to all group members." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "SyncPlayIsInGroup", ++ "SyncPlayHasAccess", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Endpoint": { ++ "get": { ++ "tags": [ ++ "System" ++ ], ++ "summary": "Gets information about the request endpoint.", ++ "operationId": "GetEndpointInfo", ++ "responses": { ++ "200": { ++ "description": "Information retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/EndPointInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/EndPointInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/EndPointInfo" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to get endpoint information.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Info": { ++ "get": { ++ "tags": [ ++ "System" ++ ], ++ "summary": "Gets information about the server.", ++ "operationId": "GetSystemInfo", ++ "responses": { ++ "200": { ++ "description": "Information retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/SystemInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SystemInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SystemInfo" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to retrieve information.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "FirstTimeSetupOrIgnoreParentalControl", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Info/Public": { ++ "get": { ++ "tags": [ ++ "System" ++ ], ++ "summary": "Gets public information about the server.", ++ "operationId": "GetPublicSystemInfo", ++ "responses": { ++ "200": { ++ "description": "Information retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/PublicSystemInfo" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PublicSystemInfo" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PublicSystemInfo" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/System/Info/Storage": { ++ "get": { ++ "tags": [ ++ "System" ++ ], ++ "summary": "Gets information about the server.", ++ "operationId": "GetSystemStorage", ++ "responses": { ++ "200": { ++ "description": "Information retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/SystemStorageDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SystemStorageDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/SystemStorageDto" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to retrieve information.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Logs": { ++ "get": { ++ "tags": [ ++ "System" ++ ], ++ "summary": "Gets a list of available server log files.", ++ "operationId": "GetServerLogs", ++ "responses": { ++ "200": { ++ "description": "Information retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LogFile" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LogFile" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LogFile" ++ } ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to get server logs.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Logs/Log": { ++ "get": { ++ "tags": [ ++ "System" ++ ], ++ "summary": "Gets a log file.", ++ "operationId": "GetLogFile", ++ "parameters": [ ++ { ++ "name": "name", ++ "in": "query", ++ "description": "The name of the log file to get.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Log file retrieved.", ++ "content": { ++ "text/plain": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User does not have permission to get log files.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Could not find a log file with the name.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Ping": { ++ "get": { ++ "tags": [ ++ "System" ++ ], ++ "summary": "Pings the system.", ++ "operationId": "GetPingSystem", ++ "responses": { ++ "200": { ++ "description": "Information retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "string" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "post": { ++ "tags": [ ++ "System" ++ ], ++ "summary": "Pings the system.", ++ "operationId": "PostPingSystem", ++ "responses": { ++ "200": { ++ "description": "Information retrieved.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "string" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "string" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/System/Restart": { ++ "post": { ++ "tags": [ ++ "System" ++ ], ++ "summary": "Restarts the application.", ++ "operationId": "RestartApplication", ++ "responses": { ++ "204": { ++ "description": "Server restarted." ++ }, ++ "403": { ++ "description": "User does not have permission to restart server.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "LocalAccessOrRequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/System/Shutdown": { ++ "post": { ++ "tags": [ ++ "System" ++ ], ++ "summary": "Shuts down the application.", ++ "operationId": "ShutdownApplication", ++ "responses": { ++ "204": { ++ "description": "Server shut down." ++ }, ++ "403": { ++ "description": "User does not have permission to shutdown server.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/GetUtcTime": { ++ "get": { ++ "tags": [ ++ "TimeSync" ++ ], ++ "summary": "Gets the current UTC time.", ++ "operationId": "GetUtcTime", ++ "responses": { ++ "200": { ++ "description": "Time returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UtcTimeResponse" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UtcTimeResponse" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UtcTimeResponse" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Tmdb/ClientConfiguration": { ++ "get": { ++ "tags": [ ++ "Tmdb" ++ ], ++ "summary": "Gets the TMDb image configuration options.", ++ "operationId": "TmdbClientConfiguration", ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ConfigImageTypes" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Trailers": { ++ "get": { ++ "tags": [ ++ "Trailers" ++ ], ++ "summary": "Finds movies and trailers similar to a given trailer.", ++ "operationId": "GetTrailers", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id supplied as query parameter; this is required when not using an API key.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "maxOfficialRating", ++ "in": "query", ++ "description": "Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "hasThemeSong", ++ "in": "query", ++ "description": "Optional filter by items with theme songs.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasThemeVideo", ++ "in": "query", ++ "description": "Optional filter by items with theme videos.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasSubtitles", ++ "in": "query", ++ "description": "Optional filter by items with subtitles.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasSpecialFeature", ++ "in": "query", ++ "description": "Optional filter by items with special features.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasTrailer", ++ "in": "query", ++ "description": "Optional filter by items with trailers.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "adjacentTo", ++ "in": "query", ++ "description": "Optional. Return items that are siblings of a supplied item.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "parentIndexNumber", ++ "in": "query", ++ "description": "Optional filter by parent index number.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "hasParentalRating", ++ "in": "query", ++ "description": "Optional filter by items that have or do not have a parental rating.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isHd", ++ "in": "query", ++ "description": "Optional filter by items that are HD or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "is4K", ++ "in": "query", ++ "description": "Optional filter by items that are 4K or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "locationTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LocationType" ++ } ++ } ++ }, ++ { ++ "name": "excludeLocationTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LocationType" ++ } ++ } ++ }, ++ { ++ "name": "isMissing", ++ "in": "query", ++ "description": "Optional filter by items that are missing episodes or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isUnaired", ++ "in": "query", ++ "description": "Optional filter by items that are unaired episodes or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "minCommunityRating", ++ "in": "query", ++ "description": "Optional filter by minimum community rating.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "minCriticRating", ++ "in": "query", ++ "description": "Optional filter by minimum critic rating.", ++ "schema": { ++ "type": "number", ++ "format": "double" ++ } ++ }, ++ { ++ "name": "minPremiereDate", ++ "in": "query", ++ "description": "Optional. The minimum premiere date. Format = ISO.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "minDateLastSaved", ++ "in": "query", ++ "description": "Optional. The minimum last saved date. Format = ISO.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "minDateLastSavedForUser", ++ "in": "query", ++ "description": "Optional. The minimum last saved date for the current user. Format = ISO.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "maxPremiereDate", ++ "in": "query", ++ "description": "Optional. The maximum premiere date. Format = ISO.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "hasOverview", ++ "in": "query", ++ "description": "Optional filter by items that have an overview or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasImdbId", ++ "in": "query", ++ "description": "Optional filter by items that have an IMDb id or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasTmdbId", ++ "in": "query", ++ "description": "Optional filter by items that have a TMDb id or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasTvdbId", ++ "in": "query", ++ "description": "Optional filter by items that have a TVDb id or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isMovie", ++ "in": "query", ++ "description": "Optional filter for live tv movies.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSeries", ++ "in": "query", ++ "description": "Optional filter for live tv series.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isNews", ++ "in": "query", ++ "description": "Optional filter for live tv news.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isKids", ++ "in": "query", ++ "description": "Optional filter for live tv kids.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isSports", ++ "in": "query", ++ "description": "Optional filter for live tv sports.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "excludeItemIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "recursive", ++ "in": "query", ++ "description": "When searching within folders, this determines whether or not the search will be recursive. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "searchTerm", ++ "in": "query", ++ "description": "Optional. Filter based on a search term.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Sort Order - Ascending, Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "excludeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "filters", ++ "in": "query", ++ "description": "Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFilter" ++ } ++ } ++ }, ++ { ++ "name": "isFavorite", ++ "in": "query", ++ "description": "Optional filter by items that are marked as favorite, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "mediaTypes", ++ "in": "query", ++ "description": "Optional filter by MediaType. Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ } ++ }, ++ { ++ "name": "imageTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "isPlayed", ++ "in": "query", ++ "description": "Optional filter by items that are played, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "genres", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "officialRatings", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "tags", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "years", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional, include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional, the max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "person", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified person.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "personIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified person id.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "personTypes", ++ "in": "query", ++ "description": "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "studios", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "artists", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "excludeArtistIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "artistIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified artist id.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "albumArtistIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified album artist id.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "contributingArtistIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "albums", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "albumIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "ids", ++ "in": "query", ++ "description": "Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "videoTypes", ++ "in": "query", ++ "description": "Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/VideoType" ++ } ++ } ++ }, ++ { ++ "name": "minOfficialRating", ++ "in": "query", ++ "description": "Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "isLocked", ++ "in": "query", ++ "description": "Optional filter by items that are locked.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isPlaceHolder", ++ "in": "query", ++ "description": "Optional filter by items that are placeholders.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "hasOfficialRating", ++ "in": "query", ++ "description": "Optional filter by items that have official ratings.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "collapseBoxSetItems", ++ "in": "query", ++ "description": "Whether or not to hide items behind their boxsets.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "minWidth", ++ "in": "query", ++ "description": "Optional. Filter by the minimum width of the item.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minHeight", ++ "in": "query", ++ "description": "Optional. Filter by the minimum height of the item.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. Filter by the maximum width of the item.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. Filter by the maximum height of the item.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "is3D", ++ "in": "query", ++ "description": "Optional filter by items that are 3D, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "seriesStatus", ++ "in": "query", ++ "description": "Optional filter by Series Status. Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SeriesStatus" ++ } ++ } ++ }, ++ { ++ "name": "nameStartsWithOrGreater", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally or greater than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameStartsWith", ++ "in": "query", ++ "description": "Optional filter by items whose name is sorted equally than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "nameLessThan", ++ "in": "query", ++ "description": "Optional filter by items whose name is equally or lesser than a given input string.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "studioIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "genreIds", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Optional. Enable the total record count.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional, include image information in output.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/Trickplay/{width}/{index}.jpg": { ++ "get": { ++ "tags": [ ++ "Trickplay" ++ ], ++ "summary": "Gets a trickplay tile image.", ++ "operationId": "GetTrickplayTileImage", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "path", ++ "description": "The width of a single tile.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "index", ++ "in": "path", ++ "description": "The index of the desired tile.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if using an alternate version.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Tile image not found at specified index.", ++ "content": { ++ "image/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Not Found", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/Trickplay/{width}/tiles.m3u8": { ++ "get": { ++ "tags": [ ++ "Trickplay" ++ ], ++ "summary": "Gets an image tiles playlist for trickplay.", ++ "operationId": "GetTrickplayHlsPlaylist", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "path", ++ "description": "The width of a single tile.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if using an alternate version.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Tiles playlist returned.", ++ "content": { ++ "application/x-mpegURL": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Not Found", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Shows/{seriesId}/Episodes": { ++ "get": { ++ "tags": [ ++ "TvShows" ++ ], ++ "summary": "Gets episodes for a tv season.", ++ "operationId": "GetEpisodes", ++ "parameters": [ ++ { ++ "name": "seriesId", ++ "in": "path", ++ "description": "The series id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "season", ++ "in": "query", ++ "description": "Optional filter by season number.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "seasonId", ++ "in": "query", ++ "description": "Optional. Filter by season id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "isMissing", ++ "in": "query", ++ "description": "Optional. Filter by items that are missing episodes or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "adjacentTo", ++ "in": "query", ++ "description": "Optional. Return items that are siblings of a supplied item.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startItemId", ++ "in": "query", ++ "description": "Optional. Skip through the list until a given item is found.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional, include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional, the max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", ++ "schema": { ++ "enum": [ ++ "Default", ++ "AiredEpisodeOrder", ++ "Album", ++ "AlbumArtist", ++ "Artist", ++ "DateCreated", ++ "OfficialRating", ++ "DatePlayed", ++ "PremiereDate", ++ "StartDate", ++ "SortName", ++ "Name", ++ "Random", ++ "Runtime", ++ "CommunityRating", ++ "ProductionYear", ++ "PlayCount", ++ "CriticRating", ++ "IsFolder", ++ "IsUnplayed", ++ "IsPlayed", ++ "SeriesSortName", ++ "VideoBitRate", ++ "AirTime", ++ "Studio", ++ "IsFavoriteOrLiked", ++ "DateLastContentAdded", ++ "SeriesDatePlayed", ++ "ParentIndexNumber", ++ "IndexNumber" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ ] ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Not Found", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Shows/{seriesId}/Seasons": { ++ "get": { ++ "tags": [ ++ "TvShows" ++ ], ++ "summary": "Gets seasons for a tv series.", ++ "operationId": "GetSeasons", ++ "parameters": [ ++ { ++ "name": "seriesId", ++ "in": "path", ++ "description": "The series id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "isSpecialSeason", ++ "in": "query", ++ "description": "Optional. Filter by special season.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isMissing", ++ "in": "query", ++ "description": "Optional. Filter by items that are missing episodes or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "adjacentTo", ++ "in": "query", ++ "description": "Optional. Return items that are siblings of a supplied item.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Not Found", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Shows/NextUp": { ++ "get": { ++ "tags": [ ++ "TvShows" ++ ], ++ "summary": "Gets a list of next up episodes.", ++ "operationId": "GetNextUp", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id of the user to get the next up episodes for.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "seriesId", ++ "in": "query", ++ "description": "Optional. Filter by series id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "nextUpDateCutoff", ++ "in": "query", ++ "description": "Optional. Starting date of shows to show in Next Up section.", ++ "schema": { ++ "type": "string", ++ "format": "date-time" ++ } ++ }, ++ { ++ "name": "enableTotalRecordCount", ++ "in": "query", ++ "description": "Whether to enable the total records count. Defaults to true.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "disableFirstEpisode", ++ "in": "query", ++ "description": "Whether to disable sending the first episode in a series as next up.", ++ "deprecated": true, ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "enableResumable", ++ "in": "query", ++ "description": "Whether to include resumable episodes in next up results.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableRewatching", ++ "in": "query", ++ "description": "Whether to include watched episodes in next up results.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Shows/Upcoming": { ++ "get": { ++ "tags": [ ++ "TvShows" ++ ], ++ "summary": "Gets a list of upcoming episodes.", ++ "operationId": "GetUpcomingEpisodes", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id of the user to get the upcoming episodes for.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Optional. The record index to start at. All items with a lower index will be dropped from the results.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Success", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Audio/{itemId}/universal": { ++ "get": { ++ "tags": [ ++ "UniversalAudio" ++ ], ++ "summary": "Gets an audio stream.", ++ "operationId": "GetUniversalAudioStream", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "query", ++ "description": "Optional. The audio container.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. The audio codec to transcode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "transcodingAudioChannels", ++ "in": "query", ++ "description": "Optional. The number of how many audio channels to transcode to.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxStreamingBitrate", ++ "in": "query", ++ "description": "Optional. The maximum streaming bitrate.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "transcodingContainer", ++ "in": "query", ++ "description": "Optional. The container to transcode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodingProtocol", ++ "in": "query", ++ "description": "Optional. The transcoding protocol.", ++ "schema": { ++ "enum": [ ++ "http", ++ "hls" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaStreamProtocol" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxAudioSampleRate", ++ "in": "query", ++ "description": "Optional. The maximum audio sample rate.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableRemoteMedia", ++ "in": "query", ++ "description": "Optional. Whether to enable remote media.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "enableRedirection", ++ "in": "query", ++ "description": "Whether to enable redirection. Defaults to true.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Audio stream returned.", ++ "content": { ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "302": { ++ "description": "Redirected to remote audio stream." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "head": { ++ "tags": [ ++ "UniversalAudio" ++ ], ++ "summary": "Gets an audio stream.", ++ "operationId": "HeadUniversalAudioStream", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "query", ++ "description": "Optional. The audio container.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. The audio codec to transcode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "transcodingAudioChannels", ++ "in": "query", ++ "description": "Optional. The number of how many audio channels to transcode to.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxStreamingBitrate", ++ "in": "query", ++ "description": "Optional. The maximum streaming bitrate.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "transcodingContainer", ++ "in": "query", ++ "description": "Optional. The container to transcode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodingProtocol", ++ "in": "query", ++ "description": "Optional. The transcoding protocol.", ++ "schema": { ++ "enum": [ ++ "http", ++ "hls" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaStreamProtocol" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxAudioSampleRate", ++ "in": "query", ++ "description": "Optional. The maximum audio sample rate.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableRemoteMedia", ++ "in": "query", ++ "description": "Optional. Whether to enable remote media.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ }, ++ { ++ "name": "enableRedirection", ++ "in": "query", ++ "description": "Whether to enable redirection. Defaults to true.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Audio stream returned.", ++ "content": { ++ "audio/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "302": { ++ "description": "Redirected to remote audio stream." ++ }, ++ "404": { ++ "description": "Item not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Users": { ++ "get": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Gets a list of users.", ++ "operationId": "GetUsers", ++ "parameters": [ ++ { ++ "name": "isHidden", ++ "in": "query", ++ "description": "Optional filter by IsHidden=true or false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "isDisabled", ++ "in": "query", ++ "description": "Optional filter by IsDisabled=true or false.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Users returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Updates a user.", ++ "operationId": "UpdateUser", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The updated user model.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ ], ++ "description": "Class UserDto." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ ], ++ "description": "Class UserDto." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ ], ++ "description": "Class UserDto." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "User updated." ++ }, ++ "400": { ++ "description": "User information was not supplied.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User update forbidden.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Users/{userId}": { ++ "get": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Gets a user by Id.", ++ "operationId": "GetUserById", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "path", ++ "description": "The user id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "User returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "User not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "IgnoreParentalControl", ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Deletes a user.", ++ "operationId": "DeleteUser", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "path", ++ "description": "The user id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "User deleted." ++ }, ++ "404": { ++ "description": "User not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Users/{userId}/Policy": { ++ "post": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Updates a user policy.", ++ "operationId": "UpdateUserPolicy", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "path", ++ "description": "The user id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The new user policy.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserPolicy" ++ } ++ ] ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserPolicy" ++ } ++ ] ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserPolicy" ++ } ++ ] ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "User policy updated." ++ }, ++ "400": { ++ "description": "User policy was not supplied.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "403": { ++ "description": "User policy update forbidden.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Users/AuthenticateByName": { ++ "post": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Authenticates a user by name.", ++ "operationId": "AuthenticateUserByName", ++ "requestBody": { ++ "description": "The M:Jellyfin.Api.Controllers.UserController.AuthenticateUserByName(Jellyfin.Api.Models.UserDtos.AuthenticateUserByName) request.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AuthenticateUserByName" ++ } ++ ], ++ "description": "The authenticate user by name request body." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AuthenticateUserByName" ++ } ++ ], ++ "description": "The authenticate user by name request body." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AuthenticateUserByName" ++ } ++ ], ++ "description": "The authenticate user by name request body." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "User authenticated.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/AuthenticationResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/AuthenticationResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/AuthenticationResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Users/AuthenticateWithQuickConnect": { ++ "post": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Authenticates a user with quick connect.", ++ "operationId": "AuthenticateWithQuickConnect", ++ "requestBody": { ++ "description": "The Jellyfin.Api.Models.UserDtos.QuickConnectDto request.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/QuickConnectDto" ++ } ++ ], ++ "description": "The quick connect request body." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/QuickConnectDto" ++ } ++ ], ++ "description": "The quick connect request body." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/QuickConnectDto" ++ } ++ ], ++ "description": "The quick connect request body." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "User authenticated.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/AuthenticationResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/AuthenticationResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/AuthenticationResult" ++ } ++ } ++ } ++ }, ++ "400": { ++ "description": "Missing token." ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Users/Configuration": { ++ "post": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Updates a user configuration.", ++ "operationId": "UpdateUserConfiguration", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The new user configuration.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserConfiguration" ++ } ++ ], ++ "description": "Class UserConfiguration." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserConfiguration" ++ } ++ ], ++ "description": "Class UserConfiguration." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserConfiguration" ++ } ++ ], ++ "description": "Class UserConfiguration." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "User configuration updated." ++ }, ++ "403": { ++ "description": "User configuration update forbidden.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Users/ForgotPassword": { ++ "post": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Initiates the forgot password process for a local user.", ++ "operationId": "ForgotPassword", ++ "requestBody": { ++ "description": "The forgot password request containing the entered username.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ForgotPasswordDto" ++ } ++ ], ++ "description": "Forgot Password request body DTO." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ForgotPasswordDto" ++ } ++ ], ++ "description": "Forgot Password request body DTO." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ForgotPasswordDto" ++ } ++ ], ++ "description": "Forgot Password request body DTO." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Password reset process started.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ForgotPasswordResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ForgotPasswordResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ForgotPasswordResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Users/ForgotPassword/Pin": { ++ "post": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Redeems a forgot password pin.", ++ "operationId": "ForgotPasswordPin", ++ "requestBody": { ++ "description": "The forgot password pin request containing the entered pin.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ForgotPasswordPinDto" ++ } ++ ], ++ "description": "Forgot Password Pin enter request body DTO." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ForgotPasswordPinDto" ++ } ++ ], ++ "description": "Forgot Password Pin enter request body DTO." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ForgotPasswordPinDto" ++ } ++ ], ++ "description": "Forgot Password Pin enter request body DTO." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "Pin reset process started.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/PinRedeemResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PinRedeemResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/PinRedeemResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Users/Me": { ++ "get": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Gets the user based on auth token.", ++ "operationId": "GetCurrentUser", ++ "responses": { ++ "200": { ++ "description": "User returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ } ++ } ++ }, ++ "400": { ++ "description": "Token is not owned by a user.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Users/New": { ++ "post": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Creates a user.", ++ "operationId": "CreateUserByName", ++ "requestBody": { ++ "description": "The create user by name request body.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CreateUserByName" ++ } ++ ], ++ "description": "The create user by name request body." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CreateUserByName" ++ } ++ ], ++ "description": "The create user by name request body." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CreateUserByName" ++ } ++ ], ++ "description": "The create user by name request body." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "200": { ++ "description": "User created.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Users/Password": { ++ "post": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Updates a user's password.", ++ "operationId": "UpdateUserPassword", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "The user id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "requestBody": { ++ "description": "The M:Jellyfin.Api.Controllers.UserController.UpdateUserPassword(System.Nullable{System.Guid},Jellyfin.Api.Models.UserDtos.UpdateUserPassword) request.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateUserPassword" ++ } ++ ], ++ "description": "The update user password request body." ++ } ++ }, ++ "text/json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateUserPassword" ++ } ++ ], ++ "description": "The update user password request body." ++ } ++ }, ++ "application/*+json": { ++ "schema": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UpdateUserPassword" ++ } ++ ], ++ "description": "The update user password request body." ++ } ++ } ++ }, ++ "required": true ++ }, ++ "responses": { ++ "204": { ++ "description": "Password successfully reset." ++ }, ++ "403": { ++ "description": "User is not allowed to update the password.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "User not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Users/Public": { ++ "get": { ++ "tags": [ ++ "User" ++ ], ++ "summary": "Gets a list of publicly visible users for display on a login screen.", ++ "operationId": "GetPublicUsers", ++ "responses": { ++ "200": { ++ "description": "Public users returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Items/{itemId}/Intros": { ++ "get": { ++ "tags": [ ++ "UserLibrary" ++ ], ++ "summary": "Gets intros to play before the main media item plays.", ++ "operationId": "GetIntros", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Intros returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/LocalTrailers": { ++ "get": { ++ "tags": [ ++ "UserLibrary" ++ ], ++ "summary": "Gets local trailers for an item.", ++ "operationId": "GetLocalTrailers", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "An Microsoft.AspNetCore.Mvc.OkResult containing the item's local trailers.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/{itemId}/SpecialFeatures": { ++ "get": { ++ "tags": [ ++ "UserLibrary" ++ ], ++ "summary": "Gets special features for an item.", ++ "operationId": "GetSpecialFeatures", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Special features returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/Latest": { ++ "get": { ++ "tags": [ ++ "UserLibrary" ++ ], ++ "summary": "Gets latest media.", ++ "operationId": "GetLatestMedia", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "isPlayed", ++ "in": "query", ++ "description": "Filter by items that are played, or not.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. include image information in output.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. the max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Return item limit.", ++ "schema": { ++ "type": "integer", ++ "format": "int32", ++ "default": 20 ++ } ++ }, ++ { ++ "name": "groupItems", ++ "in": "query", ++ "description": "Whether or not to group items into a parent container.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Latest media returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Items/Root": { ++ "get": { ++ "tags": [ ++ "UserLibrary" ++ ], ++ "summary": "Gets the root folder from a user's library.", ++ "operationId": "GetRootFolder", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Root folder returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/UserFavoriteItems/{itemId}": { ++ "post": { ++ "tags": [ ++ "UserLibrary" ++ ], ++ "summary": "Marks an item as a favorite.", ++ "operationId": "MarkFavoriteItem", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Item marked as favorite.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "delete": { ++ "tags": [ ++ "UserLibrary" ++ ], ++ "summary": "Unmarks item as a favorite.", ++ "operationId": "UnmarkFavoriteItem", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Item unmarked as favorite.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/UserItems/{itemId}/Rating": { ++ "delete": { ++ "tags": [ ++ "UserLibrary" ++ ], ++ "summary": "Deletes a user's saved personal rating for an item.", ++ "operationId": "DeleteUserItemRating", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Personal rating removed.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ }, ++ "post": { ++ "tags": [ ++ "UserLibrary" ++ ], ++ "summary": "Updates a user's rating for an item.", ++ "operationId": "UpdateUserItemRating", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "Item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "likes", ++ "in": "query", ++ "description": "Whether this M:Jellyfin.Api.Controllers.UserLibraryController.UpdateUserItemRating(System.Nullable{System.Guid},System.Guid,System.Nullable{System.Boolean}) is likes.", ++ "schema": { ++ "type": "boolean" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Item rating updated.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/UserViews": { ++ "get": { ++ "tags": [ ++ "UserViews" ++ ], ++ "summary": "Get user views.", ++ "operationId": "GetUserViews", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "includeExternalContent", ++ "in": "query", ++ "description": "Whether or not to include external views such as channels or live tv.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "presetViews", ++ "in": "query", ++ "description": "Preset views.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CollectionType" ++ } ++ } ++ }, ++ { ++ "name": "includeHidden", ++ "in": "query", ++ "description": "Whether or not to include hidden content.", ++ "schema": { ++ "type": "boolean", ++ "default": false ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "User views returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/UserViews/GroupingOptions": { ++ "get": { ++ "tags": [ ++ "UserViews" ++ ], ++ "summary": "Get user view grouping options.", ++ "operationId": "GetGroupingOptions", ++ "parameters": [ ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "User view grouping options returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SpecialViewOptionDto" ++ } ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SpecialViewOptionDto" ++ } ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SpecialViewOptionDto" ++ } ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "User not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{videoId}/{mediaSourceId}/Attachments/{index}": { ++ "get": { ++ "tags": [ ++ "VideoAttachments" ++ ], ++ "summary": "Get video attachment.", ++ "operationId": "GetAttachment", ++ "parameters": [ ++ { ++ "name": "videoId", ++ "in": "path", ++ "description": "Video ID.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "path", ++ "description": "Media Source ID.", ++ "required": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "index", ++ "in": "path", ++ "description": "Attachment Index.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Attachment retrieved.", ++ "content": { ++ "application/octet-stream": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Video or attachment not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Videos/{itemId}/AdditionalParts": { ++ "get": { ++ "tags": [ ++ "Videos" ++ ], ++ "summary": "Gets additional parts for a video.", ++ "operationId": "GetAdditionalPart", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Additional parts returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/AlternateSources": { ++ "delete": { ++ "tags": [ ++ "Videos" ++ ], ++ "summary": "Removes alternate video sources.", ++ "operationId": "DeleteAlternateSources", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Alternate sources deleted." ++ }, ++ "404": { ++ "description": "Video not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Videos/{itemId}/stream": { ++ "get": { ++ "tags": [ ++ "Videos" ++ ], ++ "summary": "Gets a video stream.", ++ "operationId": "GetVideoStream", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "query", ++ "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. The maximum horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. The maximum vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Video stream returned.", ++ "content": { ++ "video/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Videos" ++ ], ++ "summary": "Gets a video stream.", ++ "operationId": "HeadVideoStream", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "query", ++ "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "deprecated": true, ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. The maximum horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. The maximum vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Video stream returned.", ++ "content": { ++ "video/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Videos/{itemId}/stream.{container}": { ++ "get": { ++ "tags": [ ++ "Videos" ++ ], ++ "summary": "Gets a video stream.", ++ "operationId": "GetVideoStreamByContainer", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "path", ++ "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", ++ "required": true, ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. The maximum horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. The maximum vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Video stream returned.", ++ "content": { ++ "video/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ }, ++ "head": { ++ "tags": [ ++ "Videos" ++ ], ++ "summary": "Gets a video stream.", ++ "operationId": "HeadVideoStreamByContainer", ++ "parameters": [ ++ { ++ "name": "itemId", ++ "in": "path", ++ "description": "The item id.", ++ "required": true, ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "container", ++ "in": "path", ++ "description": "The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv.", ++ "required": true, ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "static", ++ "in": "query", ++ "description": "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "params", ++ "in": "query", ++ "description": "The streaming parameters.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "tag", ++ "in": "query", ++ "description": "The tag.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceProfileId", ++ "in": "query", ++ "description": "Optional. The dlna device profile id to utilize.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "playSessionId", ++ "in": "query", ++ "description": "The play session id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentContainer", ++ "in": "query", ++ "description": "The segment container.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "segmentLength", ++ "in": "query", ++ "description": "The segment length.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "minSegments", ++ "in": "query", ++ "description": "The minimum number of segments.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "mediaSourceId", ++ "in": "query", ++ "description": "The media version id, if playing an alternate version.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "deviceId", ++ "in": "query", ++ "description": "The device id of the client requesting. Used to stop encoding processes when needed.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioCodec", ++ "in": "query", ++ "description": "Optional. Specify an audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableAutoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowVideoStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the video stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "allowAudioStreamCopy", ++ "in": "query", ++ "description": "Whether or not to allow copying of the audio stream url.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "breakOnNonKeyFrames", ++ "in": "query", ++ "description": "Optional. Whether to break on non key frames.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "audioSampleRate", ++ "in": "query", ++ "description": "Optional. Specify a specific audio sample rate, e.g. 44100.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum audio bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioBitRate", ++ "in": "query", ++ "description": "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "audioChannels", ++ "in": "query", ++ "description": "Optional. Specify a specific number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxAudioChannels", ++ "in": "query", ++ "description": "Optional. Specify a maximum number of audio channels to encode to, e.g. 2.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "profile", ++ "in": "query", ++ "description": "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "level", ++ "in": "query", ++ "description": "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", ++ "schema": { ++ "pattern": "-?[0-9]+(?:\\.[0-9]+)?", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "framerate", ++ "in": "query", ++ "description": "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "maxFramerate", ++ "in": "query", ++ "description": "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", ++ "schema": { ++ "type": "number", ++ "format": "float" ++ } ++ }, ++ { ++ "name": "copyTimestamps", ++ "in": "query", ++ "description": "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "startTimeTicks", ++ "in": "query", ++ "description": "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.", ++ "schema": { ++ "type": "integer", ++ "format": "int64" ++ } ++ }, ++ { ++ "name": "width", ++ "in": "query", ++ "description": "Optional. The fixed horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "height", ++ "in": "query", ++ "description": "Optional. The fixed vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxWidth", ++ "in": "query", ++ "description": "Optional. The maximum horizontal resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxHeight", ++ "in": "query", ++ "description": "Optional. The maximum vertical resolution of the encoded video.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoBitRate", ++ "in": "query", ++ "description": "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "subtitleMethod", ++ "in": "query", ++ "description": "Optional. Specify the subtitle delivery method.", ++ "schema": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "maxRefFrames", ++ "in": "query", ++ "description": "Optional.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "maxVideoBitDepth", ++ "in": "query", ++ "description": "Optional. The maximum video bit depth.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "requireAvc", ++ "in": "query", ++ "description": "Optional. Whether to require avc.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "deInterlace", ++ "in": "query", ++ "description": "Optional. Whether to deinterlace the video.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "requireNonAnamorphic", ++ "in": "query", ++ "description": "Optional. Whether to require a non anamorphic stream.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "transcodingMaxAudioChannels", ++ "in": "query", ++ "description": "Optional. The maximum number of audio channels to transcode.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "cpuCoreLimit", ++ "in": "query", ++ "description": "Optional. The limit of how many cpu cores to use.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "liveStreamId", ++ "in": "query", ++ "description": "The live stream id.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "enableMpegtsM2TsMode", ++ "in": "query", ++ "description": "Optional. Whether to enable the MpegtsM2Ts mode.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "videoCodec", ++ "in": "query", ++ "description": "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "subtitleCodec", ++ "in": "query", ++ "description": "Optional. Specify a subtitle codec to encode to.", ++ "schema": { ++ "pattern": "^[a-zA-Z0-9\\-\\._,|]{0,40}$", ++ "type": "string" ++ } ++ }, ++ { ++ "name": "transcodeReasons", ++ "in": "query", ++ "description": "Optional. The transcoding reason.", ++ "schema": { ++ "type": "string" ++ } ++ }, ++ { ++ "name": "audioStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "videoStreamIndex", ++ "in": "query", ++ "description": "Optional. The index of the video stream to use. If omitted the first video stream will be used.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "context", ++ "in": "query", ++ "description": "Optional. The MediaBrowser.Model.Dlna.EncodingContext.", ++ "schema": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ] ++ } ++ }, ++ { ++ "name": "streamOptions", ++ "in": "query", ++ "description": "Optional. The streaming options.", ++ "schema": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ { ++ "name": "enableAudioVbrEncoding", ++ "in": "query", ++ "description": "Optional. Whether to enable Audio Encoding.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Video stream returned.", ++ "content": { ++ "video/*": { ++ "schema": { ++ "type": "string", ++ "format": "binary" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ } ++ } ++ } ++ }, ++ "/Videos/MergeVersions": { ++ "post": { ++ "tags": [ ++ "Videos" ++ ], ++ "summary": "Merges videos into a single record.", ++ "operationId": "MergeVersions", ++ "parameters": [ ++ { ++ "name": "ids", ++ "in": "query", ++ "description": "Item id list. This allows multiple, comma delimited.", ++ "required": true, ++ "schema": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ } ++ ], ++ "responses": { ++ "204": { ++ "description": "Videos merged." ++ }, ++ "400": { ++ "description": "Supply at least 2 video ids.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "RequiresElevation" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Years": { ++ "get": { ++ "tags": [ ++ "Years" ++ ], ++ "summary": "Get years.", ++ "operationId": "GetYears", ++ "parameters": [ ++ { ++ "name": "startIndex", ++ "in": "query", ++ "description": "Skips over a given number of items within the results. Use for paging.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "limit", ++ "in": "query", ++ "description": "Optional. The maximum number of records to return.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "sortOrder", ++ "in": "query", ++ "description": "Sort Order - Ascending,Descending.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ } ++ }, ++ { ++ "name": "parentId", ++ "in": "query", ++ "description": "Specify this to localize the search to a specific item or folder. Omit to use the root.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "fields", ++ "in": "query", ++ "description": "Optional. Specify additional fields of information to return in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ } ++ } ++ }, ++ { ++ "name": "excludeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "includeItemTypes", ++ "in": "query", ++ "description": "Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ } ++ }, ++ { ++ "name": "mediaTypes", ++ "in": "query", ++ "description": "Optional. Filter by MediaType. Allows multiple, comma delimited.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ } ++ }, ++ { ++ "name": "sortBy", ++ "in": "query", ++ "description": "Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ } ++ } ++ }, ++ { ++ "name": "enableUserData", ++ "in": "query", ++ "description": "Optional. Include user data.", ++ "schema": { ++ "type": "boolean" ++ } ++ }, ++ { ++ "name": "imageTypeLimit", ++ "in": "query", ++ "description": "Optional. The max number of images to return, per image type.", ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "enableImageTypes", ++ "in": "query", ++ "description": "Optional. The image types to include in the output.", ++ "schema": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "User Id.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ { ++ "name": "recursive", ++ "in": "query", ++ "description": "Search recursively.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ }, ++ { ++ "name": "enableImages", ++ "in": "query", ++ "description": "Optional. Include image information in output.", ++ "schema": { ++ "type": "boolean", ++ "default": true ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Year query returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDtoQueryResult" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ }, ++ "/Years/{year}": { ++ "get": { ++ "tags": [ ++ "Years" ++ ], ++ "summary": "Gets a year.", ++ "operationId": "GetYear", ++ "parameters": [ ++ { ++ "name": "year", ++ "in": "path", ++ "description": "The year.", ++ "required": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ { ++ "name": "userId", ++ "in": "query", ++ "description": "Optional. Filter by user id, and attach user data.", ++ "schema": { ++ "type": "string", ++ "format": "uuid" ++ } ++ } ++ ], ++ "responses": { ++ "200": { ++ "description": "Year returned.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ } ++ } ++ }, ++ "404": { ++ "description": "Year not found.", ++ "content": { ++ "application/json": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"CamelCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ }, ++ "application/json; profile=\"PascalCase\"": { ++ "schema": { ++ "$ref": "#/components/schemas/ProblemDetails" ++ } ++ } ++ } ++ }, ++ "503": { ++ "description": "The server is currently starting or is temporarily not available.", ++ "headers": { ++ "Retry-After": { ++ "description": "A hint for when to retry the operation in full seconds.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "integer", ++ "format": "int32" ++ } ++ }, ++ "Message": { ++ "description": "A short plain-text reason why the server is not available.", ++ "allowEmptyValue": true, ++ "schema": { ++ "type": "string", ++ "format": "text" ++ } ++ } ++ }, ++ "content": { ++ "text/html": { } ++ } ++ }, ++ "401": { ++ "description": "Unauthorized" ++ }, ++ "403": { ++ "description": "Forbidden" ++ } ++ }, ++ "security": [ ++ { ++ "CustomAuthentication": [ ++ "DefaultAuthorization" ++ ] ++ } ++ ] ++ } ++ } ++ }, ++ "components": { ++ "schemas": { ++ "AccessSchedule": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "integer", ++ "description": "Gets the id of this instance.", ++ "format": "int32", ++ "readOnly": true ++ }, ++ "UserId": { ++ "type": "string", ++ "description": "Gets the id of the associated user.", ++ "format": "uuid" ++ }, ++ "DayOfWeek": { ++ "enum": [ ++ "Sunday", ++ "Monday", ++ "Tuesday", ++ "Wednesday", ++ "Thursday", ++ "Friday", ++ "Saturday", ++ "Everyday", ++ "Weekday", ++ "Weekend" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DynamicDayOfWeek" ++ } ++ ], ++ "description": "Gets or sets the day of week." ++ }, ++ "StartHour": { ++ "type": "number", ++ "description": "Gets or sets the start hour.", ++ "format": "double" ++ }, ++ "EndHour": { ++ "type": "number", ++ "description": "Gets or sets the end hour.", ++ "format": "double" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "An entity representing a user's access schedule." ++ }, ++ "ActivityLogEntry": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "integer", ++ "description": "Gets or sets the identifier.", ++ "format": "int64" ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name." ++ }, ++ "Overview": { ++ "type": "string", ++ "description": "Gets or sets the overview.", ++ "nullable": true ++ }, ++ "ShortOverview": { ++ "type": "string", ++ "description": "Gets or sets the short overview.", ++ "nullable": true ++ }, ++ "Type": { ++ "type": "string", ++ "description": "Gets or sets the type." ++ }, ++ "ItemId": { ++ "type": "string", ++ "description": "Gets or sets the item identifier.", ++ "nullable": true ++ }, ++ "Date": { ++ "type": "string", ++ "description": "Gets or sets the date.", ++ "format": "date-time" ++ }, ++ "UserId": { ++ "type": "string", ++ "description": "Gets or sets the user identifier.", ++ "format": "uuid" ++ }, ++ "UserPrimaryImageTag": { ++ "type": "string", ++ "description": "Gets or sets the user primary image tag.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "Severity": { ++ "enum": [ ++ "Trace", ++ "Debug", ++ "Information", ++ "Warning", ++ "Error", ++ "Critical", ++ "None" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/LogLevel" ++ } ++ ], ++ "description": "Gets or sets the log severity." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "An activity log entry." ++ }, ++ "ActivityLogEntryMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ActivityLogEntry" ++ }, ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "ActivityLogEntry", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Activity log created message." ++ }, ++ "ActivityLogEntryQueryResult": { ++ "type": "object", ++ "properties": { ++ "Items": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ActivityLogEntry" ++ }, ++ "description": "Gets or sets the items." ++ }, ++ "TotalRecordCount": { ++ "type": "integer", ++ "description": "Gets or sets the total number of records available.", ++ "format": "int32" ++ }, ++ "StartIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the first record in Items.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Query result container." ++ }, ++ "ActivityLogEntryStartMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "type": "string", ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "ActivityLogEntryStart", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Activity log entry start message.\r\nData is the timing data encoded as \"$initialDelay,$interval\" in ms." ++ }, ++ "ActivityLogEntryStopMessage": { ++ "type": "object", ++ "properties": { ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "ActivityLogEntryStop", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Activity log entry stop message." ++ }, ++ "AddVirtualFolderDto": { ++ "type": "object", ++ "properties": { ++ "LibraryOptions": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/LibraryOptions" ++ } ++ ], ++ "description": "Gets or sets library options.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Add virtual folder dto." ++ }, ++ "AlbumInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "description": "Gets or sets the original title.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "MetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "Year": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsAutomated": { ++ "type": "boolean" ++ }, ++ "AlbumArtists": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the album artist." ++ }, ++ "ArtistProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the artist provider ids." ++ }, ++ "SongInfos": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SongInfo" ++ } ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "AlbumInfoRemoteSearchQuery": { ++ "type": "object", ++ "properties": { ++ "SearchInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AlbumInfo" ++ } ++ ], ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "SearchProviderName": { ++ "type": "string", ++ "description": "Gets or sets the provider name to search within if set.", ++ "nullable": true ++ }, ++ "IncludeDisabledProviders": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether disabled providers should be included." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "AllThemeMediaResult": { ++ "type": "object", ++ "properties": { ++ "ThemeVideosResult": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ThemeMediaResult" ++ } ++ ], ++ "description": "Class ThemeMediaResult.", ++ "nullable": true ++ }, ++ "ThemeSongsResult": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ThemeMediaResult" ++ } ++ ], ++ "description": "Class ThemeMediaResult.", ++ "nullable": true ++ }, ++ "SoundtrackSongsResult": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ThemeMediaResult" ++ } ++ ], ++ "description": "Class ThemeMediaResult.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "ArtistInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "description": "Gets or sets the original title.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "MetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "Year": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsAutomated": { ++ "type": "boolean" ++ }, ++ "SongInfos": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SongInfo" ++ } ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "ArtistInfoRemoteSearchQuery": { ++ "type": "object", ++ "properties": { ++ "SearchInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ArtistInfo" ++ } ++ ], ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "SearchProviderName": { ++ "type": "string", ++ "description": "Gets or sets the provider name to search within if set.", ++ "nullable": true ++ }, ++ "IncludeDisabledProviders": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether disabled providers should be included." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "AudioSpatialFormat": { ++ "enum": [ ++ "None", ++ "DolbyAtmos", ++ "DTSX" ++ ], ++ "type": "string", ++ "description": "An enum representing formats of spatial audio." ++ }, ++ "AuthenticateUserByName": { ++ "type": "object", ++ "properties": { ++ "Username": { ++ "type": "string", ++ "description": "Gets or sets the username.", ++ "nullable": true ++ }, ++ "Pw": { ++ "type": "string", ++ "description": "Gets or sets the plain text password.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The authenticate user by name request body." ++ }, ++ "AuthenticationInfo": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "integer", ++ "description": "Gets or sets the identifier.", ++ "format": "int64" ++ }, ++ "AccessToken": { ++ "type": "string", ++ "description": "Gets or sets the access token.", ++ "nullable": true ++ }, ++ "DeviceId": { ++ "type": "string", ++ "description": "Gets or sets the device identifier.", ++ "nullable": true ++ }, ++ "AppName": { ++ "type": "string", ++ "description": "Gets or sets the name of the application.", ++ "nullable": true ++ }, ++ "AppVersion": { ++ "type": "string", ++ "description": "Gets or sets the application version.", ++ "nullable": true ++ }, ++ "DeviceName": { ++ "type": "string", ++ "description": "Gets or sets the name of the device.", ++ "nullable": true ++ }, ++ "UserId": { ++ "type": "string", ++ "description": "Gets or sets the user identifier.", ++ "format": "uuid" ++ }, ++ "IsActive": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is active." ++ }, ++ "DateCreated": { ++ "type": "string", ++ "description": "Gets or sets the date created.", ++ "format": "date-time" ++ }, ++ "DateRevoked": { ++ "type": "string", ++ "description": "Gets or sets the date revoked.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "DateLastActivity": { ++ "type": "string", ++ "format": "date-time" ++ }, ++ "UserName": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "AuthenticationInfoQueryResult": { ++ "type": "object", ++ "properties": { ++ "Items": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/AuthenticationInfo" ++ }, ++ "description": "Gets or sets the items." ++ }, ++ "TotalRecordCount": { ++ "type": "integer", ++ "description": "Gets or sets the total number of records available.", ++ "format": "int32" ++ }, ++ "StartIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the first record in Items.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Query result container." ++ }, ++ "AuthenticationResult": { ++ "type": "object", ++ "properties": { ++ "User": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ ], ++ "description": "Class UserDto.", ++ "nullable": true ++ }, ++ "SessionInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionInfoDto" ++ } ++ ], ++ "description": "Session info DTO.", ++ "nullable": true ++ }, ++ "AccessToken": { ++ "type": "string", ++ "description": "Gets or sets the access token.", ++ "nullable": true ++ }, ++ "ServerId": { ++ "type": "string", ++ "description": "Gets or sets the server id.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "A class representing an authentication result." ++ }, ++ "BackupManifestDto": { ++ "type": "object", ++ "properties": { ++ "ServerVersion": { ++ "type": "string", ++ "description": "Gets or sets the jellyfin version this backup was created with." ++ }, ++ "BackupEngineVersion": { ++ "type": "string", ++ "description": "Gets or sets the backup engine version this backup was created with." ++ }, ++ "DateCreated": { ++ "type": "string", ++ "description": "Gets or sets the date this backup was created with.", ++ "format": "date-time" ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path to the backup on the system." ++ }, ++ "Options": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BackupOptionsDto" ++ } ++ ], ++ "description": "Gets or sets the contents of the backup archive." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Manifest type for backups internal structure." ++ }, ++ "BackupOptionsDto": { ++ "type": "object", ++ "properties": { ++ "Metadata": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the archive contains the Metadata contents." ++ }, ++ "Trickplay": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the archive contains the Trickplay contents." ++ }, ++ "Subtitles": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the archive contains the Subtitle contents." ++ }, ++ "Database": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the archive contains the Database contents." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines the optional contents of the backup archive." ++ }, ++ "BackupRestoreRequestDto": { ++ "type": "object", ++ "properties": { ++ "ArchiveFileName": { ++ "type": "string", ++ "description": "Gets or Sets the name of the backup archive to restore from. Must be present in MediaBrowser.Common.Configuration.IApplicationPaths.BackupPath." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines properties used to start a restore process." ++ }, ++ "BaseItemDto": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ServerId": { ++ "type": "string", ++ "description": "Gets or sets the server identifier.", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the id.", ++ "format": "uuid" ++ }, ++ "Etag": { ++ "type": "string", ++ "description": "Gets or sets the etag.", ++ "nullable": true ++ }, ++ "SourceType": { ++ "type": "string", ++ "description": "Gets or sets the type of the source.", ++ "nullable": true ++ }, ++ "PlaylistItemId": { ++ "type": "string", ++ "description": "Gets or sets the playlist item identifier.", ++ "nullable": true ++ }, ++ "DateCreated": { ++ "type": "string", ++ "description": "Gets or sets the date created.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "DateLastMediaAdded": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "ExtraType": { ++ "enum": [ ++ "Unknown", ++ "Clip", ++ "Trailer", ++ "BehindTheScenes", ++ "DeletedScene", ++ "Interview", ++ "Scene", ++ "Sample", ++ "ThemeSong", ++ "ThemeVideo", ++ "Featurette", ++ "Short" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ExtraType" ++ } ++ ], ++ "nullable": true ++ }, ++ "AirsBeforeSeasonNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "AirsAfterSeasonNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "AirsBeforeEpisodeNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "CanDelete": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "CanDownload": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "HasLyrics": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "HasSubtitles": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "PreferredMetadataLanguage": { ++ "type": "string", ++ "nullable": true ++ }, ++ "PreferredMetadataCountryCode": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Container": { ++ "type": "string", ++ "nullable": true ++ }, ++ "SortName": { ++ "type": "string", ++ "description": "Gets or sets the name of the sort.", ++ "nullable": true ++ }, ++ "ForcedSortName": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Video3DFormat": { ++ "enum": [ ++ "HalfSideBySide", ++ "FullSideBySide", ++ "FullTopAndBottom", ++ "HalfTopAndBottom", ++ "MVC" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/Video3DFormat" ++ } ++ ], ++ "description": "Gets or sets the video3 D format.", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "description": "Gets or sets the premiere date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "ExternalUrls": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ExternalUrl" ++ }, ++ "description": "Gets or sets the external urls.", ++ "nullable": true ++ }, ++ "MediaSources": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaSourceInfo" ++ }, ++ "description": "Gets or sets the media versions.", ++ "nullable": true ++ }, ++ "CriticRating": { ++ "type": "number", ++ "description": "Gets or sets the critic rating.", ++ "format": "float", ++ "nullable": true ++ }, ++ "ProductionLocations": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "EnableMediaSourceDisplay": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "OfficialRating": { ++ "type": "string", ++ "description": "Gets or sets the official rating.", ++ "nullable": true ++ }, ++ "CustomRating": { ++ "type": "string", ++ "description": "Gets or sets the custom rating.", ++ "nullable": true ++ }, ++ "ChannelId": { ++ "type": "string", ++ "description": "Gets or sets the channel identifier.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "ChannelName": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Overview": { ++ "type": "string", ++ "description": "Gets or sets the overview.", ++ "nullable": true ++ }, ++ "Taglines": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the taglines.", ++ "nullable": true ++ }, ++ "Genres": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the genres.", ++ "nullable": true ++ }, ++ "CommunityRating": { ++ "type": "number", ++ "description": "Gets or sets the community rating.", ++ "format": "float", ++ "nullable": true ++ }, ++ "CumulativeRunTimeTicks": { ++ "type": "integer", ++ "description": "Gets or sets the cumulative run time ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "RunTimeTicks": { ++ "type": "integer", ++ "description": "Gets or sets the run time ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "PlayAccess": { ++ "enum": [ ++ "Full", ++ "None" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayAccess" ++ } ++ ], ++ "description": "Gets or sets the play access.", ++ "nullable": true ++ }, ++ "AspectRatio": { ++ "type": "string", ++ "description": "Gets or sets the aspect ratio.", ++ "nullable": true ++ }, ++ "ProductionYear": { ++ "type": "integer", ++ "description": "Gets or sets the production year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IsPlaceHolder": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is place holder.", ++ "nullable": true ++ }, ++ "Number": { ++ "type": "string", ++ "description": "Gets or sets the number.", ++ "nullable": true ++ }, ++ "ChannelNumber": { ++ "type": "string", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "description": "Gets or sets the index number.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumberEnd": { ++ "type": "integer", ++ "description": "Gets or sets the index number end.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "description": "Gets or sets the parent index number.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "RemoteTrailers": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaUrl" ++ }, ++ "description": "Gets or sets the trailer urls.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "IsHD": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is HD.", ++ "nullable": true ++ }, ++ "IsFolder": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is folder.", ++ "nullable": true ++ }, ++ "ParentId": { ++ "type": "string", ++ "description": "Gets or sets the parent id.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "Type": { ++ "enum": [ ++ "AggregateFolder", ++ "Audio", ++ "AudioBook", ++ "BasePluginFolder", ++ "Book", ++ "BoxSet", ++ "Channel", ++ "ChannelFolderItem", ++ "CollectionFolder", ++ "Episode", ++ "Folder", ++ "Genre", ++ "ManualPlaylistsFolder", ++ "Movie", ++ "LiveTvChannel", ++ "LiveTvProgram", ++ "MusicAlbum", ++ "MusicArtist", ++ "MusicGenre", ++ "MusicVideo", ++ "Person", ++ "Photo", ++ "PhotoAlbum", ++ "Playlist", ++ "PlaylistsFolder", ++ "Program", ++ "Recording", ++ "Season", ++ "Series", ++ "Studio", ++ "Trailer", ++ "TvChannel", ++ "TvProgram", ++ "UserRootFolder", ++ "UserView", ++ "Video", ++ "Year" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ ], ++ "description": "Gets or sets the type." ++ }, ++ "People": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemPerson" ++ }, ++ "description": "Gets or sets the people.", ++ "nullable": true ++ }, ++ "Studios": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameGuidPair" ++ }, ++ "description": "Gets or sets the studios.", ++ "nullable": true ++ }, ++ "GenreItems": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameGuidPair" ++ }, ++ "nullable": true ++ }, ++ "ParentLogoItemId": { ++ "type": "string", ++ "description": "Gets or sets whether the item has a logo, this will hold the Id of the Parent that has one.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "ParentBackdropItemId": { ++ "type": "string", ++ "description": "Gets or sets whether the item has any backdrops, this will hold the Id of the Parent that has one.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "ParentBackdropImageTags": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the parent backdrop image tags.", ++ "nullable": true ++ }, ++ "LocalTrailerCount": { ++ "type": "integer", ++ "description": "Gets or sets the local trailer count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "UserData": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ } ++ ], ++ "description": "Gets or sets the user data for this item based on the user it's being requested for.", ++ "nullable": true ++ }, ++ "RecursiveItemCount": { ++ "type": "integer", ++ "description": "Gets or sets the recursive item count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ChildCount": { ++ "type": "integer", ++ "description": "Gets or sets the child count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SeriesName": { ++ "type": "string", ++ "description": "Gets or sets the name of the series.", ++ "nullable": true ++ }, ++ "SeriesId": { ++ "type": "string", ++ "description": "Gets or sets the series id.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "SeasonId": { ++ "type": "string", ++ "description": "Gets or sets the season identifier.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "SpecialFeatureCount": { ++ "type": "integer", ++ "description": "Gets or sets the special feature count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "DisplayPreferencesId": { ++ "type": "string", ++ "description": "Gets or sets the display preferences id.", ++ "nullable": true ++ }, ++ "Status": { ++ "type": "string", ++ "description": "Gets or sets the status.", ++ "nullable": true ++ }, ++ "AirTime": { ++ "type": "string", ++ "description": "Gets or sets the air time.", ++ "nullable": true ++ }, ++ "AirDays": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/DayOfWeek" ++ }, ++ "description": "Gets or sets the air days.", ++ "nullable": true ++ }, ++ "Tags": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the tags.", ++ "nullable": true ++ }, ++ "PrimaryImageAspectRatio": { ++ "type": "number", ++ "description": "Gets or sets the primary image aspect ratio, after image enhancements.", ++ "format": "double", ++ "nullable": true ++ }, ++ "Artists": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the artists.", ++ "nullable": true ++ }, ++ "ArtistItems": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameGuidPair" ++ }, ++ "description": "Gets or sets the artist items.", ++ "nullable": true ++ }, ++ "Album": { ++ "type": "string", ++ "description": "Gets or sets the album.", ++ "nullable": true ++ }, ++ "CollectionType": { ++ "enum": [ ++ "unknown", ++ "movies", ++ "tvshows", ++ "music", ++ "musicvideos", ++ "trailers", ++ "homevideos", ++ "boxsets", ++ "books", ++ "photos", ++ "livetv", ++ "playlists", ++ "folders" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CollectionType" ++ } ++ ], ++ "description": "Gets or sets the type of the collection.", ++ "nullable": true ++ }, ++ "DisplayOrder": { ++ "type": "string", ++ "description": "Gets or sets the display order.", ++ "nullable": true ++ }, ++ "AlbumId": { ++ "type": "string", ++ "description": "Gets or sets the album id.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "AlbumPrimaryImageTag": { ++ "type": "string", ++ "description": "Gets or sets the album image tag.", ++ "nullable": true ++ }, ++ "SeriesPrimaryImageTag": { ++ "type": "string", ++ "description": "Gets or sets the series primary image tag.", ++ "nullable": true ++ }, ++ "AlbumArtist": { ++ "type": "string", ++ "description": "Gets or sets the album artist.", ++ "nullable": true ++ }, ++ "AlbumArtists": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameGuidPair" ++ }, ++ "description": "Gets or sets the album artists.", ++ "nullable": true ++ }, ++ "SeasonName": { ++ "type": "string", ++ "description": "Gets or sets the name of the season.", ++ "nullable": true ++ }, ++ "MediaStreams": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaStream" ++ }, ++ "description": "Gets or sets the media streams.", ++ "nullable": true ++ }, ++ "VideoType": { ++ "enum": [ ++ "VideoFile", ++ "Iso", ++ "Dvd", ++ "BluRay" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/VideoType" ++ } ++ ], ++ "description": "Gets or sets the type of the video.", ++ "nullable": true ++ }, ++ "PartCount": { ++ "type": "integer", ++ "description": "Gets or sets the part count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MediaSourceCount": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ImageTags": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the image tags.", ++ "nullable": true ++ }, ++ "BackdropImageTags": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the backdrop image tags.", ++ "nullable": true ++ }, ++ "ScreenshotImageTags": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the screenshot image tags.", ++ "nullable": true ++ }, ++ "ParentLogoImageTag": { ++ "type": "string", ++ "description": "Gets or sets the parent logo image tag.", ++ "nullable": true ++ }, ++ "ParentArtItemId": { ++ "type": "string", ++ "description": "Gets or sets whether the item has fan art, this will hold the Id of the Parent that has one.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "ParentArtImageTag": { ++ "type": "string", ++ "description": "Gets or sets the parent art image tag.", ++ "nullable": true ++ }, ++ "SeriesThumbImageTag": { ++ "type": "string", ++ "description": "Gets or sets the series thumb image tag.", ++ "nullable": true ++ }, ++ "ImageBlurHashes": { ++ "type": "object", ++ "properties": { ++ "Primary": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Art": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Backdrop": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Banner": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Logo": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Thumb": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Disc": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Box": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Screenshot": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Menu": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Chapter": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "BoxRear": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Profile": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ } ++ }, ++ "description": "Gets or sets the blurhashes for the image tags.\r\nMaps image type to dictionary mapping image tag to blurhash value.", ++ "nullable": true ++ }, ++ "SeriesStudio": { ++ "type": "string", ++ "description": "Gets or sets the series studio.", ++ "nullable": true ++ }, ++ "ParentThumbItemId": { ++ "type": "string", ++ "description": "Gets or sets the parent thumb item id.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "ParentThumbImageTag": { ++ "type": "string", ++ "description": "Gets or sets the parent thumb image tag.", ++ "nullable": true ++ }, ++ "ParentPrimaryImageItemId": { ++ "type": "string", ++ "description": "Gets or sets the parent primary image item identifier.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "ParentPrimaryImageTag": { ++ "type": "string", ++ "description": "Gets or sets the parent primary image tag.", ++ "nullable": true ++ }, ++ "Chapters": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ChapterInfo" ++ }, ++ "description": "Gets or sets the chapters.", ++ "nullable": true ++ }, ++ "Trickplay": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "object", ++ "additionalProperties": { ++ "$ref": "#/components/schemas/TrickplayInfoDto" ++ } ++ }, ++ "description": "Gets or sets the trickplay manifest.", ++ "nullable": true ++ }, ++ "LocationType": { ++ "enum": [ ++ "FileSystem", ++ "Remote", ++ "Virtual", ++ "Offline" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/LocationType" ++ } ++ ], ++ "description": "Gets or sets the type of the location.", ++ "nullable": true ++ }, ++ "IsoType": { ++ "enum": [ ++ "Dvd", ++ "BluRay" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/IsoType" ++ } ++ ], ++ "description": "Gets or sets the type of the iso.", ++ "nullable": true ++ }, ++ "MediaType": { ++ "enum": [ ++ "Unknown", ++ "Video", ++ "Audio", ++ "Photo", ++ "Book" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ ], ++ "description": "Gets or sets the type of the media.", ++ "default": "Unknown" ++ }, ++ "EndDate": { ++ "type": "string", ++ "description": "Gets or sets the end date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "LockedFields": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MetadataField" ++ }, ++ "description": "Gets or sets the locked fields.", ++ "nullable": true ++ }, ++ "TrailerCount": { ++ "type": "integer", ++ "description": "Gets or sets the trailer count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MovieCount": { ++ "type": "integer", ++ "description": "Gets or sets the movie count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SeriesCount": { ++ "type": "integer", ++ "description": "Gets or sets the series count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ProgramCount": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "EpisodeCount": { ++ "type": "integer", ++ "description": "Gets or sets the episode count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SongCount": { ++ "type": "integer", ++ "description": "Gets or sets the song count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "AlbumCount": { ++ "type": "integer", ++ "description": "Gets or sets the album count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ArtistCount": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MusicVideoCount": { ++ "type": "integer", ++ "description": "Gets or sets the music video count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "LockData": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [enable internet providers].", ++ "nullable": true ++ }, ++ "Width": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Height": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "CameraMake": { ++ "type": "string", ++ "nullable": true ++ }, ++ "CameraModel": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Software": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ExposureTime": { ++ "type": "number", ++ "format": "double", ++ "nullable": true ++ }, ++ "FocalLength": { ++ "type": "number", ++ "format": "double", ++ "nullable": true ++ }, ++ "ImageOrientation": { ++ "enum": [ ++ "TopLeft", ++ "TopRight", ++ "BottomRight", ++ "BottomLeft", ++ "LeftTop", ++ "RightTop", ++ "RightBottom", ++ "LeftBottom" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageOrientation" ++ } ++ ], ++ "nullable": true ++ }, ++ "Aperture": { ++ "type": "number", ++ "format": "double", ++ "nullable": true ++ }, ++ "ShutterSpeed": { ++ "type": "number", ++ "format": "double", ++ "nullable": true ++ }, ++ "Latitude": { ++ "type": "number", ++ "format": "double", ++ "nullable": true ++ }, ++ "Longitude": { ++ "type": "number", ++ "format": "double", ++ "nullable": true ++ }, ++ "Altitude": { ++ "type": "number", ++ "format": "double", ++ "nullable": true ++ }, ++ "IsoSpeedRating": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SeriesTimerId": { ++ "type": "string", ++ "description": "Gets or sets the series timer identifier.", ++ "nullable": true ++ }, ++ "ProgramId": { ++ "type": "string", ++ "description": "Gets or sets the program identifier.", ++ "nullable": true ++ }, ++ "ChannelPrimaryImageTag": { ++ "type": "string", ++ "description": "Gets or sets the channel primary image tag.", ++ "nullable": true ++ }, ++ "StartDate": { ++ "type": "string", ++ "description": "Gets or sets the start date of the recording, in UTC.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "CompletionPercentage": { ++ "type": "number", ++ "description": "Gets or sets the completion percentage.", ++ "format": "double", ++ "nullable": true ++ }, ++ "IsRepeat": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is repeat.", ++ "nullable": true ++ }, ++ "EpisodeTitle": { ++ "type": "string", ++ "description": "Gets or sets the episode title.", ++ "nullable": true ++ }, ++ "ChannelType": { ++ "enum": [ ++ "TV", ++ "Radio" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ChannelType" ++ } ++ ], ++ "description": "Gets or sets the type of the channel.", ++ "nullable": true ++ }, ++ "Audio": { ++ "enum": [ ++ "Mono", ++ "Stereo", ++ "Dolby", ++ "DolbyDigital", ++ "Thx", ++ "Atmos" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ProgramAudio" ++ } ++ ], ++ "description": "Gets or sets the audio.", ++ "nullable": true ++ }, ++ "IsMovie": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is movie.", ++ "nullable": true ++ }, ++ "IsSports": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is sports.", ++ "nullable": true ++ }, ++ "IsSeries": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is series.", ++ "nullable": true ++ }, ++ "IsLive": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is live.", ++ "nullable": true ++ }, ++ "IsNews": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is news.", ++ "nullable": true ++ }, ++ "IsKids": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is kids.", ++ "nullable": true ++ }, ++ "IsPremiere": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is premiere.", ++ "nullable": true ++ }, ++ "TimerId": { ++ "type": "string", ++ "description": "Gets or sets the timer identifier.", ++ "nullable": true ++ }, ++ "NormalizationGain": { ++ "type": "number", ++ "description": "Gets or sets the gain required for audio normalization.", ++ "format": "float", ++ "nullable": true ++ }, ++ "CurrentProgram": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ ], ++ "description": "Gets or sets the current program.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "This is strictly used as a data transfer object from the api layer.\r\nThis holds information about a BaseItem in a format that is convenient for the client." ++ }, ++ "BaseItemDtoQueryResult": { ++ "type": "object", ++ "properties": { ++ "Items": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ }, ++ "description": "Gets or sets the items." ++ }, ++ "TotalRecordCount": { ++ "type": "integer", ++ "description": "Gets or sets the total number of records available.", ++ "format": "int32" ++ }, ++ "StartIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the first record in Items.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Query result container." ++ }, ++ "BaseItemKind": { ++ "enum": [ ++ "AggregateFolder", ++ "Audio", ++ "AudioBook", ++ "BasePluginFolder", ++ "Book", ++ "BoxSet", ++ "Channel", ++ "ChannelFolderItem", ++ "CollectionFolder", ++ "Episode", ++ "Folder", ++ "Genre", ++ "ManualPlaylistsFolder", ++ "Movie", ++ "LiveTvChannel", ++ "LiveTvProgram", ++ "MusicAlbum", ++ "MusicArtist", ++ "MusicGenre", ++ "MusicVideo", ++ "Person", ++ "Photo", ++ "PhotoAlbum", ++ "Playlist", ++ "PlaylistsFolder", ++ "Program", ++ "Recording", ++ "Season", ++ "Series", ++ "Studio", ++ "Trailer", ++ "TvChannel", ++ "TvProgram", ++ "UserRootFolder", ++ "UserView", ++ "Video", ++ "Year" ++ ], ++ "type": "string", ++ "description": "The base item kind." ++ }, ++ "BaseItemPerson": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the identifier.", ++ "format": "uuid" ++ }, ++ "Role": { ++ "type": "string", ++ "description": "Gets or sets the role.", ++ "nullable": true ++ }, ++ "Type": { ++ "enum": [ ++ "Unknown", ++ "Actor", ++ "Director", ++ "Composer", ++ "Writer", ++ "GuestStar", ++ "Producer", ++ "Conductor", ++ "Lyricist", ++ "Arranger", ++ "Engineer", ++ "Mixer", ++ "Remixer", ++ "Creator", ++ "Artist", ++ "AlbumArtist", ++ "Author", ++ "Illustrator", ++ "Penciller", ++ "Inker", ++ "Colorist", ++ "Letterer", ++ "CoverArtist", ++ "Editor", ++ "Translator" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PersonKind" ++ } ++ ], ++ "description": "Gets or sets the type.", ++ "default": "Unknown" ++ }, ++ "PrimaryImageTag": { ++ "type": "string", ++ "description": "Gets or sets the primary image tag.", ++ "nullable": true ++ }, ++ "ImageBlurHashes": { ++ "type": "object", ++ "properties": { ++ "Primary": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Art": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Backdrop": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Banner": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Logo": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Thumb": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Disc": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Box": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Screenshot": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Menu": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Chapter": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "BoxRear": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ }, ++ "Profile": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ } ++ } ++ }, ++ "description": "Gets or sets the primary image blurhash.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "This is used by the api to get information about a Person within a BaseItem." ++ }, ++ "BasePluginConfiguration": { ++ "type": "object", ++ "additionalProperties": false, ++ "description": "Class BasePluginConfiguration." ++ }, ++ "BookInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "description": "Gets or sets the original title.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "MetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "Year": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsAutomated": { ++ "type": "boolean" ++ }, ++ "SeriesName": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "BookInfoRemoteSearchQuery": { ++ "type": "object", ++ "properties": { ++ "SearchInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BookInfo" ++ } ++ ], ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "SearchProviderName": { ++ "type": "string", ++ "description": "Gets or sets the provider name to search within if set.", ++ "nullable": true ++ }, ++ "IncludeDisabledProviders": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether disabled providers should be included." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "BoxSetInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "description": "Gets or sets the original title.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "MetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "Year": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsAutomated": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "BoxSetInfoRemoteSearchQuery": { ++ "type": "object", ++ "properties": { ++ "SearchInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BoxSetInfo" ++ } ++ ], ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "SearchProviderName": { ++ "type": "string", ++ "description": "Gets or sets the provider name to search within if set.", ++ "nullable": true ++ }, ++ "IncludeDisabledProviders": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether disabled providers should be included." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "BrandingOptionsDto": { ++ "type": "object", ++ "properties": { ++ "LoginDisclaimer": { ++ "type": "string", ++ "description": "Gets or sets the login disclaimer.", ++ "nullable": true ++ }, ++ "CustomCss": { ++ "type": "string", ++ "description": "Gets or sets the custom CSS.", ++ "nullable": true ++ }, ++ "SplashscreenEnabled": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to enable the splashscreen." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The branding options DTO for API use.\r\nThis DTO excludes SplashscreenLocation to prevent it from being updated via API." ++ }, ++ "BufferRequestDto": { ++ "type": "object", ++ "properties": { ++ "When": { ++ "type": "string", ++ "description": "Gets or sets when the request has been made by the client.", ++ "format": "date-time" ++ }, ++ "PositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the position ticks.", ++ "format": "int64" ++ }, ++ "IsPlaying": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the client playback is unpaused." ++ }, ++ "PlaylistItemId": { ++ "type": "string", ++ "description": "Gets or sets the playlist item identifier of the playing item.", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class BufferRequestDto." ++ }, ++ "CastReceiverApplication": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the cast receiver application id." ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the cast receiver application name." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The cast receiver application model." ++ }, ++ "ChannelFeatures": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name." ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the identifier.", ++ "format": "uuid" ++ }, ++ "CanSearch": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance can search." ++ }, ++ "MediaTypes": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ChannelMediaType" ++ }, ++ "description": "Gets or sets the media types." ++ }, ++ "ContentTypes": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ChannelMediaContentType" ++ }, ++ "description": "Gets or sets the content types." ++ }, ++ "MaxPageSize": { ++ "type": "integer", ++ "description": "Gets or sets the maximum number of records the channel allows retrieving at a time.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "AutoRefreshLevels": { ++ "type": "integer", ++ "description": "Gets or sets the automatic refresh levels.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "DefaultSortFields": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ChannelItemSortField" ++ }, ++ "description": "Gets or sets the default sort orders." ++ }, ++ "SupportsSortOrderToggle": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether a sort ascending/descending toggle is supported." ++ }, ++ "SupportsLatestMedia": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [supports latest media]." ++ }, ++ "CanFilter": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance can filter." ++ }, ++ "SupportsContentDownloading": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [supports content downloading]." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "ChannelItemSortField": { ++ "enum": [ ++ "Name", ++ "CommunityRating", ++ "PremiereDate", ++ "DateCreated", ++ "Runtime", ++ "PlayCount", ++ "CommunityPlayCount" ++ ], ++ "type": "string" ++ }, ++ "ChannelMappingOptionsDto": { ++ "type": "object", ++ "properties": { ++ "TunerChannels": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TunerChannelMapping" ++ }, ++ "description": "Gets or sets list of tuner channels." ++ }, ++ "ProviderChannels": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameIdPair" ++ }, ++ "description": "Gets or sets list of provider channels." ++ }, ++ "Mappings": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameValuePair" ++ }, ++ "description": "Gets or sets list of mappings." ++ }, ++ "ProviderName": { ++ "type": "string", ++ "description": "Gets or sets provider name.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Channel mapping options dto." ++ }, ++ "ChannelMediaContentType": { ++ "enum": [ ++ "Clip", ++ "Podcast", ++ "Trailer", ++ "Movie", ++ "Episode", ++ "Song", ++ "MovieExtra", ++ "TvExtra" ++ ], ++ "type": "string" ++ }, ++ "ChannelMediaType": { ++ "enum": [ ++ "Audio", ++ "Video", ++ "Photo" ++ ], ++ "type": "string" ++ }, ++ "ChannelType": { ++ "enum": [ ++ "TV", ++ "Radio" ++ ], ++ "type": "string", ++ "description": "Enum ChannelType." ++ }, ++ "ChapterInfo": { ++ "type": "object", ++ "properties": { ++ "StartPositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the start position ticks.", ++ "format": "int64" ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "ImagePath": { ++ "type": "string", ++ "description": "Gets or sets the image path.", ++ "nullable": true ++ }, ++ "ImageDateModified": { ++ "type": "string", ++ "format": "date-time" ++ }, ++ "ImageTag": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class ChapterInfo." ++ }, ++ "ClientCapabilitiesDto": { ++ "type": "object", ++ "properties": { ++ "PlayableMediaTypes": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ }, ++ "description": "Gets or sets the list of playable media types." ++ }, ++ "SupportedCommands": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/GeneralCommandType" ++ }, ++ "description": "Gets or sets the list of supported commands." ++ }, ++ "SupportsMediaControl": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether session supports media control." ++ }, ++ "SupportsPersistentIdentifier": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether session supports a persistent identifier." ++ }, ++ "DeviceProfile": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DeviceProfile" ++ } ++ ], ++ "description": "Gets or sets the device profile.", ++ "nullable": true ++ }, ++ "AppStoreUrl": { ++ "type": "string", ++ "description": "Gets or sets the app store url.", ++ "nullable": true ++ }, ++ "IconUrl": { ++ "type": "string", ++ "description": "Gets or sets the icon url.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Client capabilities dto." ++ }, ++ "ClientLogDocumentResponseDto": { ++ "type": "object", ++ "properties": { ++ "FileName": { ++ "type": "string", ++ "description": "Gets the resulting filename." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Client log document response dto." ++ }, ++ "CodecProfile": { ++ "type": "object", ++ "properties": { ++ "Type": { ++ "enum": [ ++ "Video", ++ "VideoAudio", ++ "Audio" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CodecType" ++ } ++ ], ++ "description": "Gets or sets the MediaBrowser.Model.Dlna.CodecType which this container must meet." ++ }, ++ "Conditions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ProfileCondition" ++ }, ++ "description": "Gets or sets the list of MediaBrowser.Model.Dlna.ProfileCondition which this profile must meet." ++ }, ++ "ApplyConditions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ProfileCondition" ++ }, ++ "description": "Gets or sets the list of MediaBrowser.Model.Dlna.ProfileCondition to apply if this profile is met." ++ }, ++ "Codec": { ++ "type": "string", ++ "description": "Gets or sets the codec(s) that this profile applies to.", ++ "nullable": true ++ }, ++ "Container": { ++ "type": "string", ++ "description": "Gets or sets the container(s) which this profile will be applied to.", ++ "nullable": true ++ }, ++ "SubContainer": { ++ "type": "string", ++ "description": "Gets or sets the sub-container(s) which this profile will be applied to.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines the MediaBrowser.Model.Dlna.CodecProfile." ++ }, ++ "CodecType": { ++ "enum": [ ++ "Video", ++ "VideoAudio", ++ "Audio" ++ ], ++ "type": "string" ++ }, ++ "CollectionCreationResult": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "CollectionType": { ++ "enum": [ ++ "unknown", ++ "movies", ++ "tvshows", ++ "music", ++ "musicvideos", ++ "trailers", ++ "homevideos", ++ "boxsets", ++ "books", ++ "photos", ++ "livetv", ++ "playlists", ++ "folders" ++ ], ++ "type": "string", ++ "description": "Collection type." ++ }, ++ "CollectionTypeOptions": { ++ "enum": [ ++ "movies", ++ "tvshows", ++ "music", ++ "musicvideos", ++ "homevideos", ++ "boxsets", ++ "books", ++ "mixed" ++ ], ++ "type": "string", ++ "description": "The collection type options." ++ }, ++ "ConfigImageTypes": { ++ "type": "object", ++ "properties": { ++ "BackdropSizes": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "BaseUrl": { ++ "type": "string", ++ "nullable": true ++ }, ++ "LogoSizes": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "PosterSizes": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "ProfileSizes": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "SecureBaseUrl": { ++ "type": "string", ++ "nullable": true ++ }, ++ "StillSizes": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "ConfigurationPageInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name." ++ }, ++ "EnableInMainMenu": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the configurations page is enabled in the main menu." ++ }, ++ "MenuSection": { ++ "type": "string", ++ "description": "Gets or sets the menu section.", ++ "nullable": true ++ }, ++ "MenuIcon": { ++ "type": "string", ++ "description": "Gets or sets the menu icon.", ++ "nullable": true ++ }, ++ "DisplayName": { ++ "type": "string", ++ "description": "Gets or sets the display name.", ++ "nullable": true ++ }, ++ "PluginId": { ++ "type": "string", ++ "description": "Gets or sets the plugin id.", ++ "format": "uuid", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The configuration page info." ++ }, ++ "ContainerProfile": { ++ "type": "object", ++ "properties": { ++ "Type": { ++ "enum": [ ++ "Audio", ++ "Video", ++ "Photo", ++ "Subtitle", ++ "Lyric" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DlnaProfileType" ++ } ++ ], ++ "description": "Gets or sets the MediaBrowser.Model.Dlna.DlnaProfileType which this container must meet." ++ }, ++ "Conditions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ProfileCondition" ++ }, ++ "description": "Gets or sets the list of MediaBrowser.Model.Dlna.ProfileCondition which this container will be applied to." ++ }, ++ "Container": { ++ "type": "string", ++ "description": "Gets or sets the container(s) which this container must meet.", ++ "nullable": true ++ }, ++ "SubContainer": { ++ "type": "string", ++ "description": "Gets or sets the sub container(s) which this container must meet.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines the MediaBrowser.Model.Dlna.ContainerProfile." ++ }, ++ "CountryInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "DisplayName": { ++ "type": "string", ++ "description": "Gets or sets the display name.", ++ "nullable": true ++ }, ++ "TwoLetterISORegionName": { ++ "type": "string", ++ "description": "Gets or sets the name of the two letter ISO region.", ++ "nullable": true ++ }, ++ "ThreeLetterISORegionName": { ++ "type": "string", ++ "description": "Gets or sets the name of the three letter ISO region.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class CountryInfo." ++ }, ++ "CreatePlaylistDto": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name of the new playlist." ++ }, ++ "Ids": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "description": "Gets or sets item ids to add to the playlist." ++ }, ++ "UserId": { ++ "type": "string", ++ "description": "Gets or sets the user id.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "MediaType": { ++ "enum": [ ++ "Unknown", ++ "Video", ++ "Audio", ++ "Photo", ++ "Book" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ ], ++ "description": "Gets or sets the media type.", ++ "nullable": true ++ }, ++ "Users": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PlaylistUserPermissions" ++ }, ++ "description": "Gets or sets the playlist users." ++ }, ++ "IsPublic": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the playlist is public." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Create new playlist dto." ++ }, ++ "CreateUserByName": { ++ "required": [ ++ "Name" ++ ], ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the username." ++ }, ++ "Password": { ++ "type": "string", ++ "description": "Gets or sets the password.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The create user by name request body." ++ }, ++ "CultureDto": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets the name." ++ }, ++ "DisplayName": { ++ "type": "string", ++ "description": "Gets the display name." ++ }, ++ "TwoLetterISOLanguageName": { ++ "type": "string", ++ "description": "Gets the name of the two letter ISO language." ++ }, ++ "ThreeLetterISOLanguageName": { ++ "type": "string", ++ "description": "Gets the name of the three letter ISO language.", ++ "nullable": true, ++ "readOnly": true ++ }, ++ "ThreeLetterISOLanguageNames": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class CultureDto." ++ }, ++ "CustomDatabaseOption": { ++ "type": "object", ++ "properties": { ++ "Key": { ++ "type": "string", ++ "description": "Gets or sets the key of the value." ++ }, ++ "Value": { ++ "type": "string", ++ "description": "Gets or sets the value." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The custom value option for custom database providers." ++ }, ++ "CustomDatabaseOptions": { ++ "type": "object", ++ "properties": { ++ "PluginName": { ++ "type": "string", ++ "description": "Gets or sets the Plugin name to search for database providers." ++ }, ++ "PluginAssembly": { ++ "type": "string", ++ "description": "Gets or sets the plugin assembly to search for providers." ++ }, ++ "ConnectionString": { ++ "type": "string", ++ "description": "Gets or sets the connection string for the custom database provider." ++ }, ++ "Options": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CustomDatabaseOption" ++ }, ++ "description": "Gets or sets the list of extra options for the custom provider." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines the options for a custom database connector." ++ }, ++ "DatabaseConfigurationOptions": { ++ "type": "object", ++ "properties": { ++ "DatabaseType": { ++ "type": "string", ++ "description": "Gets or Sets the type of database jellyfin should use." ++ }, ++ "CustomProviderOptions": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CustomDatabaseOptions" ++ } ++ ], ++ "description": "Gets or sets the options required to use a custom database provider.", ++ "nullable": true ++ }, ++ "LockingBehavior": { ++ "enum": [ ++ "NoLock", ++ "Pessimistic", ++ "Optimistic" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DatabaseLockingBehaviorTypes" ++ } ++ ], ++ "description": "Gets or Sets the kind of locking behavior jellyfin should perform. Possible options are \"NoLock\", \"Pessimistic\", \"Optimistic\".\r\nDefaults to \"NoLock\"." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Options to configure jellyfins managed database." ++ }, ++ "DatabaseLockingBehaviorTypes": { ++ "enum": [ ++ "NoLock", ++ "Pessimistic", ++ "Optimistic" ++ ], ++ "type": "string", ++ "description": "Defines all possible methods for locking database access for concurrent queries." ++ }, ++ "DayOfWeek": { ++ "enum": [ ++ "Sunday", ++ "Monday", ++ "Tuesday", ++ "Wednesday", ++ "Thursday", ++ "Friday", ++ "Saturday" ++ ], ++ "type": "string" ++ }, ++ "DayPattern": { ++ "enum": [ ++ "Daily", ++ "Weekdays", ++ "Weekends" ++ ], ++ "type": "string" ++ }, ++ "DefaultDirectoryBrowserInfoDto": { ++ "type": "object", ++ "properties": { ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Default directory browser info." ++ }, ++ "DeinterlaceMethod": { ++ "enum": [ ++ "yadif", ++ "bwdif" ++ ], ++ "type": "string", ++ "description": "Enum containing deinterlace methods." ++ }, ++ "DeviceInfoDto": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "CustomName": { ++ "type": "string", ++ "description": "Gets or sets the custom name.", ++ "nullable": true ++ }, ++ "AccessToken": { ++ "type": "string", ++ "description": "Gets or sets the access token.", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the identifier.", ++ "nullable": true ++ }, ++ "LastUserName": { ++ "type": "string", ++ "description": "Gets or sets the last name of the user.", ++ "nullable": true ++ }, ++ "AppName": { ++ "type": "string", ++ "description": "Gets or sets the name of the application.", ++ "nullable": true ++ }, ++ "AppVersion": { ++ "type": "string", ++ "description": "Gets or sets the application version.", ++ "nullable": true ++ }, ++ "LastUserId": { ++ "type": "string", ++ "description": "Gets or sets the last user identifier.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "DateLastActivity": { ++ "type": "string", ++ "description": "Gets or sets the date last modified.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "Capabilities": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ClientCapabilitiesDto" ++ } ++ ], ++ "description": "Gets or sets the capabilities." ++ }, ++ "IconUrl": { ++ "type": "string", ++ "description": "Gets or sets the icon URL.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "A DTO representing device information." ++ }, ++ "DeviceInfoDtoQueryResult": { ++ "type": "object", ++ "properties": { ++ "Items": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/DeviceInfoDto" ++ }, ++ "description": "Gets or sets the items." ++ }, ++ "TotalRecordCount": { ++ "type": "integer", ++ "description": "Gets or sets the total number of records available.", ++ "format": "int32" ++ }, ++ "StartIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the first record in Items.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Query result container." ++ }, ++ "DeviceOptionsDto": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "integer", ++ "description": "Gets or sets the id.", ++ "format": "int32" ++ }, ++ "DeviceId": { ++ "type": "string", ++ "description": "Gets or sets the device id.", ++ "nullable": true ++ }, ++ "CustomName": { ++ "type": "string", ++ "description": "Gets or sets the custom name.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "A dto representing custom options for a device." ++ }, ++ "DeviceProfile": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name of this device profile. User profiles must have a unique name.", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the unique internal identifier.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "MaxStreamingBitrate": { ++ "type": "integer", ++ "description": "Gets or sets the maximum allowed bitrate for all streamed content.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MaxStaticBitrate": { ++ "type": "integer", ++ "description": "Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files).", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MusicStreamingTranscodingBitrate": { ++ "type": "integer", ++ "description": "Gets or sets the maximum allowed bitrate for transcoded music streams.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MaxStaticMusicBitrate": { ++ "type": "integer", ++ "description": "Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "DirectPlayProfiles": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/DirectPlayProfile" ++ }, ++ "description": "Gets or sets the direct play profiles." ++ }, ++ "TranscodingProfiles": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TranscodingProfile" ++ }, ++ "description": "Gets or sets the transcoding profiles." ++ }, ++ "ContainerProfiles": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ContainerProfile" ++ }, ++ "description": "Gets or sets the container profiles. Failing to meet these optional conditions causes transcoding to occur." ++ }, ++ "CodecProfiles": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CodecProfile" ++ }, ++ "description": "Gets or sets the codec profiles." ++ }, ++ "SubtitleProfiles": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SubtitleProfile" ++ }, ++ "description": "Gets or sets the subtitle profiles." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.\r\n
\r\nSpecifically, it defines the supported containers and\r\ncodecs (video and/or audio, including codec profiles and levels)\r\nthe device is able to direct play (without transcoding or remuxing),\r\nas well as which containers/codecs to transcode to in case it isn't." ++ }, ++ "DirectPlayProfile": { ++ "type": "object", ++ "properties": { ++ "Container": { ++ "type": "string", ++ "description": "Gets or sets the container." ++ }, ++ "AudioCodec": { ++ "type": "string", ++ "description": "Gets or sets the audio codec.", ++ "nullable": true ++ }, ++ "VideoCodec": { ++ "type": "string", ++ "description": "Gets or sets the video codec.", ++ "nullable": true ++ }, ++ "Type": { ++ "enum": [ ++ "Audio", ++ "Video", ++ "Photo", ++ "Subtitle", ++ "Lyric" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DlnaProfileType" ++ } ++ ], ++ "description": "Gets or sets the Dlna profile type." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines the MediaBrowser.Model.Dlna.DirectPlayProfile." ++ }, ++ "DisplayPreferencesDto": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the user id.", ++ "nullable": true ++ }, ++ "ViewType": { ++ "type": "string", ++ "description": "Gets or sets the type of the view.", ++ "nullable": true ++ }, ++ "SortBy": { ++ "type": "string", ++ "description": "Gets or sets the sort by.", ++ "nullable": true ++ }, ++ "IndexBy": { ++ "type": "string", ++ "description": "Gets or sets the index by.", ++ "nullable": true ++ }, ++ "RememberIndexing": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [remember indexing]." ++ }, ++ "PrimaryImageHeight": { ++ "type": "integer", ++ "description": "Gets or sets the height of the primary image.", ++ "format": "int32" ++ }, ++ "PrimaryImageWidth": { ++ "type": "integer", ++ "description": "Gets or sets the width of the primary image.", ++ "format": "int32" ++ }, ++ "CustomPrefs": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the custom prefs." ++ }, ++ "ScrollDirection": { ++ "enum": [ ++ "Horizontal", ++ "Vertical" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ScrollDirection" ++ } ++ ], ++ "description": "Gets or sets the scroll direction." ++ }, ++ "ShowBackdrop": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to show backdrops on this item." ++ }, ++ "RememberSorting": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [remember sorting]." ++ }, ++ "SortOrder": { ++ "enum": [ ++ "Ascending", ++ "Descending" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SortOrder" ++ } ++ ], ++ "description": "Gets or sets the sort order." ++ }, ++ "ShowSidebar": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [show sidebar]." ++ }, ++ "Client": { ++ "type": "string", ++ "description": "Gets or sets the client.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines the display preferences for any item that supports them (usually Folders)." ++ }, ++ "DlnaProfileType": { ++ "enum": [ ++ "Audio", ++ "Video", ++ "Photo", ++ "Subtitle", ++ "Lyric" ++ ], ++ "type": "string" ++ }, ++ "DownMixStereoAlgorithms": { ++ "enum": [ ++ "None", ++ "Dave750", ++ "NightmodeDialogue", ++ "Rfc7845", ++ "Ac4" ++ ], ++ "type": "string", ++ "description": "An enum representing an algorithm to downmix surround sound to stereo." ++ }, ++ "DynamicDayOfWeek": { ++ "enum": [ ++ "Sunday", ++ "Monday", ++ "Tuesday", ++ "Wednesday", ++ "Thursday", ++ "Friday", ++ "Saturday", ++ "Everyday", ++ "Weekday", ++ "Weekend" ++ ], ++ "type": "string", ++ "description": "An enum that represents a day of the week, weekdays, weekends, or all days." ++ }, ++ "EmbeddedSubtitleOptions": { ++ "enum": [ ++ "AllowAll", ++ "AllowText", ++ "AllowImage", ++ "AllowNone" ++ ], ++ "type": "string", ++ "description": "An enum representing the options to disable embedded subs." ++ }, ++ "EncoderPreset": { ++ "enum": [ ++ "auto", ++ "placebo", ++ "veryslow", ++ "slower", ++ "slow", ++ "medium", ++ "fast", ++ "faster", ++ "veryfast", ++ "superfast", ++ "ultrafast" ++ ], ++ "type": "string", ++ "description": "Enum containing encoder presets." ++ }, ++ "EncodingContext": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "type": "string" ++ }, ++ "EncodingOptions": { ++ "type": "object", ++ "properties": { ++ "EncodingThreadCount": { ++ "type": "integer", ++ "description": "Gets or sets the thread count used for encoding.", ++ "format": "int32" ++ }, ++ "TranscodingTempPath": { ++ "type": "string", ++ "description": "Gets or sets the temporary transcoding path.", ++ "nullable": true ++ }, ++ "FallbackFontPath": { ++ "type": "string", ++ "description": "Gets or sets the path to the fallback font.", ++ "nullable": true ++ }, ++ "EnableFallbackFont": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to use the fallback font." ++ }, ++ "EnableAudioVbr": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether audio VBR is enabled." ++ }, ++ "DownMixAudioBoost": { ++ "type": "number", ++ "description": "Gets or sets the audio boost applied when downmixing audio.", ++ "format": "double" ++ }, ++ "DownMixStereoAlgorithm": { ++ "enum": [ ++ "None", ++ "Dave750", ++ "NightmodeDialogue", ++ "Rfc7845", ++ "Ac4" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DownMixStereoAlgorithms" ++ } ++ ], ++ "description": "Gets or sets the algorithm used for downmixing audio to stereo." ++ }, ++ "MaxMuxingQueueSize": { ++ "type": "integer", ++ "description": "Gets or sets the maximum size of the muxing queue.", ++ "format": "int32" ++ }, ++ "EnableThrottling": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether throttling is enabled." ++ }, ++ "ThrottleDelaySeconds": { ++ "type": "integer", ++ "description": "Gets or sets the delay after which throttling happens.", ++ "format": "int32" ++ }, ++ "EnableSegmentDeletion": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether segment deletion is enabled." ++ }, ++ "SegmentKeepSeconds": { ++ "type": "integer", ++ "description": "Gets or sets seconds for which segments should be kept before being deleted.", ++ "format": "int32" ++ }, ++ "HardwareAccelerationType": { ++ "enum": [ ++ "none", ++ "amf", ++ "qsv", ++ "nvenc", ++ "v4l2m2m", ++ "vaapi", ++ "videotoolbox", ++ "rkmpp" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/HardwareAccelerationType" ++ } ++ ], ++ "description": "Gets or sets the hardware acceleration type." ++ }, ++ "EncoderAppPath": { ++ "type": "string", ++ "description": "Gets or sets the FFmpeg path as set by the user via the UI.", ++ "nullable": true ++ }, ++ "EncoderAppPathDisplay": { ++ "type": "string", ++ "description": "Gets or sets the current FFmpeg path being used by the system and displayed on the transcode page.", ++ "nullable": true ++ }, ++ "VaapiDevice": { ++ "type": "string", ++ "description": "Gets or sets the VA-API device.", ++ "nullable": true ++ }, ++ "QsvDevice": { ++ "type": "string", ++ "description": "Gets or sets the QSV device.", ++ "nullable": true ++ }, ++ "EnableTonemapping": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether tonemapping is enabled." ++ }, ++ "EnableVppTonemapping": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether VPP tonemapping is enabled." ++ }, ++ "EnableVideoToolboxTonemapping": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether videotoolbox tonemapping is enabled." ++ }, ++ "TonemappingAlgorithm": { ++ "enum": [ ++ "none", ++ "clip", ++ "linear", ++ "gamma", ++ "reinhard", ++ "hable", ++ "mobius", ++ "bt2390" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TonemappingAlgorithm" ++ } ++ ], ++ "description": "Gets or sets the tone-mapping algorithm." ++ }, ++ "TonemappingMode": { ++ "enum": [ ++ "auto", ++ "max", ++ "rgb", ++ "lum", ++ "itp" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TonemappingMode" ++ } ++ ], ++ "description": "Gets or sets the tone-mapping mode." ++ }, ++ "TonemappingRange": { ++ "enum": [ ++ "auto", ++ "tv", ++ "pc" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TonemappingRange" ++ } ++ ], ++ "description": "Gets or sets the tone-mapping range." ++ }, ++ "TonemappingDesat": { ++ "type": "number", ++ "description": "Gets or sets the tone-mapping desaturation.", ++ "format": "double" ++ }, ++ "TonemappingPeak": { ++ "type": "number", ++ "description": "Gets or sets the tone-mapping peak.", ++ "format": "double" ++ }, ++ "TonemappingParam": { ++ "type": "number", ++ "description": "Gets or sets the tone-mapping parameters.", ++ "format": "double" ++ }, ++ "VppTonemappingBrightness": { ++ "type": "number", ++ "description": "Gets or sets the VPP tone-mapping brightness.", ++ "format": "double" ++ }, ++ "VppTonemappingContrast": { ++ "type": "number", ++ "description": "Gets or sets the VPP tone-mapping contrast.", ++ "format": "double" ++ }, ++ "H264Crf": { ++ "type": "integer", ++ "description": "Gets or sets the H264 CRF.", ++ "format": "int32" ++ }, ++ "H265Crf": { ++ "type": "integer", ++ "description": "Gets or sets the H265 CRF.", ++ "format": "int32" ++ }, ++ "EncoderPreset": { ++ "enum": [ ++ "auto", ++ "placebo", ++ "veryslow", ++ "slower", ++ "slow", ++ "medium", ++ "fast", ++ "faster", ++ "veryfast", ++ "superfast", ++ "ultrafast" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncoderPreset" ++ } ++ ], ++ "description": "Gets or sets the encoder preset.", ++ "nullable": true ++ }, ++ "DeinterlaceDoubleRate": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the framerate is doubled when deinterlacing." ++ }, ++ "DeinterlaceMethod": { ++ "enum": [ ++ "yadif", ++ "bwdif" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DeinterlaceMethod" ++ } ++ ], ++ "description": "Gets or sets the deinterlace method." ++ }, ++ "EnableDecodingColorDepth10Hevc": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether 10bit HEVC decoding is enabled." ++ }, ++ "EnableDecodingColorDepth10Vp9": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether 10bit VP9 decoding is enabled." ++ }, ++ "EnableDecodingColorDepth10HevcRext": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether 8/10bit HEVC RExt decoding is enabled." ++ }, ++ "EnableDecodingColorDepth12HevcRext": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether 12bit HEVC RExt decoding is enabled." ++ }, ++ "EnableEnhancedNvdecDecoder": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the enhanced NVDEC is enabled." ++ }, ++ "PreferSystemNativeHwDecoder": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the system native hardware decoder should be used." ++ }, ++ "EnableIntelLowPowerH264HwEncoder": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the Intel H264 low-power hardware encoder should be used." ++ }, ++ "EnableIntelLowPowerHevcHwEncoder": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the Intel HEVC low-power hardware encoder should be used." ++ }, ++ "EnableHardwareEncoding": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether hardware encoding is enabled." ++ }, ++ "AllowHevcEncoding": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether HEVC encoding is enabled." ++ }, ++ "AllowAv1Encoding": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether AV1 encoding is enabled." ++ }, ++ "EnableSubtitleExtraction": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether subtitle extraction is enabled." ++ }, ++ "HardwareDecodingCodecs": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the codecs hardware encoding is used for.", ++ "nullable": true ++ }, ++ "AllowOnDemandMetadataBasedKeyframeExtractionForExtensions": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class EncodingOptions." ++ }, ++ "EndPointInfo": { ++ "type": "object", ++ "properties": { ++ "IsLocal": { ++ "type": "boolean" ++ }, ++ "IsInNetwork": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "ExternalIdInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the display name of the external id provider (IE: IMDB, MusicBrainz, etc)." ++ }, ++ "Key": { ++ "type": "string", ++ "description": "Gets or sets the unique key for this id. This key should be unique across all providers." ++ }, ++ "Type": { ++ "enum": [ ++ "Album", ++ "AlbumArtist", ++ "Artist", ++ "BoxSet", ++ "Episode", ++ "Movie", ++ "OtherArtist", ++ "Person", ++ "ReleaseGroup", ++ "Season", ++ "Series", ++ "Track", ++ "Book", ++ "Recording" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ExternalIdMediaType" ++ } ++ ], ++ "description": "Gets or sets the specific media type for this id. This is used to distinguish between the different\r\nexternal id types for providers with multiple ids.\r\nA null value indicates there is no specific media type associated with the external id, or this is the\r\ndefault id for the external provider so there is no need to specify a type.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Represents the external id information for serialization to the client." ++ }, ++ "ExternalIdMediaType": { ++ "enum": [ ++ "Album", ++ "AlbumArtist", ++ "Artist", ++ "BoxSet", ++ "Episode", ++ "Movie", ++ "OtherArtist", ++ "Person", ++ "ReleaseGroup", ++ "Season", ++ "Series", ++ "Track", ++ "Book", ++ "Recording" ++ ], ++ "type": "string", ++ "description": "The specific media type of an MediaBrowser.Model.Providers.ExternalIdInfo." ++ }, ++ "ExternalUrl": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "Url": { ++ "type": "string", ++ "description": "Gets or sets the type of the item.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "ExtraType": { ++ "enum": [ ++ "Unknown", ++ "Clip", ++ "Trailer", ++ "BehindTheScenes", ++ "DeletedScene", ++ "Interview", ++ "Scene", ++ "Sample", ++ "ThemeSong", ++ "ThemeVideo", ++ "Featurette", ++ "Short" ++ ], ++ "type": "string" ++ }, ++ "FileSystemEntryInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets the name." ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets the path." ++ }, ++ "Type": { ++ "enum": [ ++ "File", ++ "Directory", ++ "NetworkComputer", ++ "NetworkShare" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/FileSystemEntryType" ++ } ++ ], ++ "description": "Gets the type." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class FileSystemEntryInfo." ++ }, ++ "FileSystemEntryType": { ++ "enum": [ ++ "File", ++ "Directory", ++ "NetworkComputer", ++ "NetworkShare" ++ ], ++ "type": "string", ++ "description": "Enum FileSystemEntryType." ++ }, ++ "FolderStorageDto": { ++ "type": "object", ++ "properties": { ++ "Path": { ++ "type": "string", ++ "description": "Gets the path of the folder in question." ++ }, ++ "FreeSpace": { ++ "type": "integer", ++ "description": "Gets the free space of the underlying storage device of the Jellyfin.Api.Models.SystemInfoDtos.FolderStorageDto.Path.", ++ "format": "int64" ++ }, ++ "UsedSpace": { ++ "type": "integer", ++ "description": "Gets the used space of the underlying storage device of the Jellyfin.Api.Models.SystemInfoDtos.FolderStorageDto.Path.", ++ "format": "int64" ++ }, ++ "StorageType": { ++ "type": "string", ++ "description": "Gets the kind of storage device of the Jellyfin.Api.Models.SystemInfoDtos.FolderStorageDto.Path.", ++ "nullable": true ++ }, ++ "DeviceId": { ++ "type": "string", ++ "description": "Gets the Device Identifier.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Contains information about a specific folder." ++ }, ++ "FontFile": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "Size": { ++ "type": "integer", ++ "description": "Gets or sets the size.", ++ "format": "int64" ++ }, ++ "DateCreated": { ++ "type": "string", ++ "description": "Gets or sets the date created.", ++ "format": "date-time" ++ }, ++ "DateModified": { ++ "type": "string", ++ "description": "Gets or sets the date modified.", ++ "format": "date-time" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class FontFile." ++ }, ++ "ForceKeepAliveMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "type": "integer", ++ "description": "Gets or sets the data.", ++ "format": "int32" ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "ForceKeepAlive", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Force keep alive websocket messages." ++ }, ++ "ForgotPasswordAction": { ++ "enum": [ ++ "ContactAdmin", ++ "PinCode", ++ "InNetworkRequired" ++ ], ++ "type": "string" ++ }, ++ "ForgotPasswordDto": { ++ "required": [ ++ "EnteredUsername" ++ ], ++ "type": "object", ++ "properties": { ++ "EnteredUsername": { ++ "type": "string", ++ "description": "Gets or sets the entered username to have its password reset." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Forgot Password request body DTO." ++ }, ++ "ForgotPasswordPinDto": { ++ "required": [ ++ "Pin" ++ ], ++ "type": "object", ++ "properties": { ++ "Pin": { ++ "type": "string", ++ "description": "Gets or sets the entered pin to have the password reset." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Forgot Password Pin enter request body DTO." ++ }, ++ "ForgotPasswordResult": { ++ "type": "object", ++ "properties": { ++ "Action": { ++ "enum": [ ++ "ContactAdmin", ++ "PinCode", ++ "InNetworkRequired" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ForgotPasswordAction" ++ } ++ ], ++ "description": "Gets or sets the action." ++ }, ++ "PinFile": { ++ "type": "string", ++ "description": "Gets or sets the pin file.", ++ "nullable": true ++ }, ++ "PinExpirationDate": { ++ "type": "string", ++ "description": "Gets or sets the pin expiration date.", ++ "format": "date-time", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "GeneralCommand": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "enum": [ ++ "MoveUp", ++ "MoveDown", ++ "MoveLeft", ++ "MoveRight", ++ "PageUp", ++ "PageDown", ++ "PreviousLetter", ++ "NextLetter", ++ "ToggleOsd", ++ "ToggleContextMenu", ++ "Select", ++ "Back", ++ "TakeScreenshot", ++ "SendKey", ++ "SendString", ++ "GoHome", ++ "GoToSettings", ++ "VolumeUp", ++ "VolumeDown", ++ "Mute", ++ "Unmute", ++ "ToggleMute", ++ "SetVolume", ++ "SetAudioStreamIndex", ++ "SetSubtitleStreamIndex", ++ "ToggleFullscreen", ++ "DisplayContent", ++ "GoToSearch", ++ "DisplayMessage", ++ "SetRepeatMode", ++ "ChannelUp", ++ "ChannelDown", ++ "Guide", ++ "ToggleStats", ++ "PlayMediaSource", ++ "PlayTrailers", ++ "SetShuffleQueue", ++ "PlayState", ++ "PlayNext", ++ "ToggleOsdMenu", ++ "Play", ++ "SetMaxStreamingBitrate", ++ "SetPlaybackOrder" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GeneralCommandType" ++ } ++ ], ++ "description": "This exists simply to identify a set of known commands." ++ }, ++ "ControllingUserId": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "Arguments": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ } ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "GeneralCommandMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GeneralCommand" ++ } ++ ], ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "GeneralCommand", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "General command websocket message." ++ }, ++ "GeneralCommandType": { ++ "enum": [ ++ "MoveUp", ++ "MoveDown", ++ "MoveLeft", ++ "MoveRight", ++ "PageUp", ++ "PageDown", ++ "PreviousLetter", ++ "NextLetter", ++ "ToggleOsd", ++ "ToggleContextMenu", ++ "Select", ++ "Back", ++ "TakeScreenshot", ++ "SendKey", ++ "SendString", ++ "GoHome", ++ "GoToSettings", ++ "VolumeUp", ++ "VolumeDown", ++ "Mute", ++ "Unmute", ++ "ToggleMute", ++ "SetVolume", ++ "SetAudioStreamIndex", ++ "SetSubtitleStreamIndex", ++ "ToggleFullscreen", ++ "DisplayContent", ++ "GoToSearch", ++ "DisplayMessage", ++ "SetRepeatMode", ++ "ChannelUp", ++ "ChannelDown", ++ "Guide", ++ "ToggleStats", ++ "PlayMediaSource", ++ "PlayTrailers", ++ "SetShuffleQueue", ++ "PlayState", ++ "PlayNext", ++ "ToggleOsdMenu", ++ "Play", ++ "SetMaxStreamingBitrate", ++ "SetPlaybackOrder" ++ ], ++ "type": "string", ++ "description": "This exists simply to identify a set of known commands." ++ }, ++ "GetProgramsDto": { ++ "type": "object", ++ "properties": { ++ "ChannelIds": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "description": "Gets or sets the channels to return guide information for.", ++ "nullable": true ++ }, ++ "UserId": { ++ "type": "string", ++ "description": "Gets or sets optional. Filter by user id.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "MinStartDate": { ++ "type": "string", ++ "description": "Gets or sets the minimum premiere start date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "HasAired": { ++ "type": "boolean", ++ "description": "Gets or sets filter by programs that have completed airing, or not.", ++ "nullable": true ++ }, ++ "IsAiring": { ++ "type": "boolean", ++ "description": "Gets or sets filter by programs that are currently airing, or not.", ++ "nullable": true ++ }, ++ "MaxStartDate": { ++ "type": "string", ++ "description": "Gets or sets the maximum premiere start date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "MinEndDate": { ++ "type": "string", ++ "description": "Gets or sets the minimum premiere end date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "MaxEndDate": { ++ "type": "string", ++ "description": "Gets or sets the maximum premiere end date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsMovie": { ++ "type": "boolean", ++ "description": "Gets or sets filter for movies.", ++ "nullable": true ++ }, ++ "IsSeries": { ++ "type": "boolean", ++ "description": "Gets or sets filter for series.", ++ "nullable": true ++ }, ++ "IsNews": { ++ "type": "boolean", ++ "description": "Gets or sets filter for news.", ++ "nullable": true ++ }, ++ "IsKids": { ++ "type": "boolean", ++ "description": "Gets or sets filter for kids.", ++ "nullable": true ++ }, ++ "IsSports": { ++ "type": "boolean", ++ "description": "Gets or sets filter for sports.", ++ "nullable": true ++ }, ++ "StartIndex": { ++ "type": "integer", ++ "description": "Gets or sets the record index to start at. All items with a lower index will be dropped from the results.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Limit": { ++ "type": "integer", ++ "description": "Gets or sets the maximum number of records to return.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SortBy": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemSortBy" ++ }, ++ "description": "Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.", ++ "nullable": true ++ }, ++ "SortOrder": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SortOrder" ++ }, ++ "description": "Gets or sets sort order.", ++ "nullable": true ++ }, ++ "Genres": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the genres to return guide information for.", ++ "nullable": true ++ }, ++ "GenreIds": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "description": "Gets or sets the genre ids to return guide information for.", ++ "nullable": true ++ }, ++ "EnableImages": { ++ "type": "boolean", ++ "description": "Gets or sets include image information in output.", ++ "nullable": true ++ }, ++ "EnableTotalRecordCount": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether retrieve total record count.", ++ "default": true ++ }, ++ "ImageTypeLimit": { ++ "type": "integer", ++ "description": "Gets or sets the max number of images to return, per image type.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "EnableImageTypes": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ }, ++ "description": "Gets or sets the image types to include in the output.", ++ "nullable": true ++ }, ++ "EnableUserData": { ++ "type": "boolean", ++ "description": "Gets or sets include user data.", ++ "nullable": true ++ }, ++ "SeriesTimerId": { ++ "type": "string", ++ "description": "Gets or sets filter by series timer id.", ++ "nullable": true ++ }, ++ "LibrarySeriesId": { ++ "type": "string", ++ "description": "Gets or sets filter by library series id.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "Fields": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ItemFields" ++ }, ++ "description": "Gets or sets specify additional fields of information to return in the output.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Get programs dto." ++ }, ++ "GroupInfoDto": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid" ++ }, ++ "GroupName": { ++ "type": "string", ++ "description": "Gets the group name." ++ }, ++ "State": { ++ "enum": [ ++ "Idle", ++ "Waiting", ++ "Paused", ++ "Playing" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupStateType" ++ } ++ ], ++ "description": "Gets the group state." ++ }, ++ "Participants": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets the participants." ++ }, ++ "LastUpdatedAt": { ++ "type": "string", ++ "description": "Gets the date when this DTO has been created.", ++ "format": "date-time" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class GroupInfoDto." ++ }, ++ "GroupQueueMode": { ++ "enum": [ ++ "Queue", ++ "QueueNext" ++ ], ++ "type": "string", ++ "description": "Enum GroupQueueMode." ++ }, ++ "GroupRepeatMode": { ++ "enum": [ ++ "RepeatOne", ++ "RepeatAll", ++ "RepeatNone" ++ ], ++ "type": "string", ++ "description": "Enum GroupRepeatMode." ++ }, ++ "GroupShuffleMode": { ++ "enum": [ ++ "Sorted", ++ "Shuffle" ++ ], ++ "type": "string", ++ "description": "Enum GroupShuffleMode." ++ }, ++ "GroupStateType": { ++ "enum": [ ++ "Idle", ++ "Waiting", ++ "Paused", ++ "Playing" ++ ], ++ "type": "string", ++ "description": "Enum GroupState." ++ }, ++ "GroupStateUpdate": { ++ "type": "object", ++ "properties": { ++ "State": { ++ "enum": [ ++ "Idle", ++ "Waiting", ++ "Paused", ++ "Playing" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupStateType" ++ } ++ ], ++ "description": "Gets the state of the group." ++ }, ++ "Reason": { ++ "enum": [ ++ "Play", ++ "SetPlaylistItem", ++ "RemoveFromPlaylist", ++ "MovePlaylistItem", ++ "Queue", ++ "Unpause", ++ "Pause", ++ "Stop", ++ "Seek", ++ "Buffer", ++ "Ready", ++ "NextItem", ++ "PreviousItem", ++ "SetRepeatMode", ++ "SetShuffleMode", ++ "Ping", ++ "IgnoreWait" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackRequestType" ++ } ++ ], ++ "description": "Gets the reason of the state change." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class GroupStateUpdate." ++ }, ++ "GroupUpdate": { ++ "type": "object", ++ "oneOf": [ ++ { ++ "$ref": "#/components/schemas/SyncPlayGroupDoesNotExistUpdate" ++ }, ++ { ++ "$ref": "#/components/schemas/SyncPlayGroupJoinedUpdate" ++ }, ++ { ++ "$ref": "#/components/schemas/SyncPlayGroupLeftUpdate" ++ }, ++ { ++ "$ref": "#/components/schemas/SyncPlayLibraryAccessDeniedUpdate" ++ }, ++ { ++ "$ref": "#/components/schemas/SyncPlayNotInGroupUpdate" ++ }, ++ { ++ "$ref": "#/components/schemas/SyncPlayPlayQueueUpdate" ++ }, ++ { ++ "$ref": "#/components/schemas/SyncPlayStateUpdate" ++ }, ++ { ++ "$ref": "#/components/schemas/SyncPlayUserJoinedUpdate" ++ }, ++ { ++ "$ref": "#/components/schemas/SyncPlayUserLeftUpdate" ++ } ++ ], ++ "description": "Represents the list of possible group update types", ++ "discriminator": { ++ "propertyName": "Type", ++ "mapping": { ++ "GroupDoesNotExist": "#/components/schemas/SyncPlayGroupDoesNotExistUpdate", ++ "GroupJoined": "#/components/schemas/SyncPlayGroupJoinedUpdate", ++ "GroupLeft": "#/components/schemas/SyncPlayGroupLeftUpdate", ++ "LibraryAccessDenied": "#/components/schemas/SyncPlayLibraryAccessDeniedUpdate", ++ "NotInGroup": "#/components/schemas/SyncPlayNotInGroupUpdate", ++ "PlayQueue": "#/components/schemas/SyncPlayPlayQueueUpdate", ++ "StateUpdate": "#/components/schemas/SyncPlayStateUpdate", ++ "UserJoined": "#/components/schemas/SyncPlayUserJoinedUpdate", ++ "UserLeft": "#/components/schemas/SyncPlayUserLeftUpdate" ++ } ++ } ++ }, ++ "GroupUpdateType": { ++ "enum": [ ++ "UserJoined", ++ "UserLeft", ++ "GroupJoined", ++ "GroupLeft", ++ "StateUpdate", ++ "PlayQueue", ++ "NotInGroup", ++ "GroupDoesNotExist", ++ "LibraryAccessDenied" ++ ], ++ "type": "string", ++ "description": "Enum GroupUpdateType." ++ }, ++ "GuideInfo": { ++ "type": "object", ++ "properties": { ++ "StartDate": { ++ "type": "string", ++ "description": "Gets or sets the start date.", ++ "format": "date-time" ++ }, ++ "EndDate": { ++ "type": "string", ++ "description": "Gets or sets the end date.", ++ "format": "date-time" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "HardwareAccelerationType": { ++ "enum": [ ++ "none", ++ "amf", ++ "qsv", ++ "nvenc", ++ "v4l2m2m", ++ "vaapi", ++ "videotoolbox", ++ "rkmpp" ++ ], ++ "type": "string", ++ "description": "Enum containing hardware acceleration types." ++ }, ++ "IgnoreWaitRequestDto": { ++ "type": "object", ++ "properties": { ++ "IgnoreWait": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the client should be ignored." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class IgnoreWaitRequestDto." ++ }, ++ "ImageFormat": { ++ "enum": [ ++ "Bmp", ++ "Gif", ++ "Jpg", ++ "Png", ++ "Webp", ++ "Svg" ++ ], ++ "type": "string", ++ "description": "Enum ImageOutputFormat." ++ }, ++ "ImageInfo": { ++ "type": "object", ++ "properties": { ++ "ImageType": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Gets or sets the type of the image." ++ }, ++ "ImageIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the image.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ImageTag": { ++ "type": "string", ++ "description": "Gets or sets the image tag.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "BlurHash": { ++ "type": "string", ++ "description": "Gets or sets the blurhash.", ++ "nullable": true ++ }, ++ "Height": { ++ "type": "integer", ++ "description": "Gets or sets the height.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Width": { ++ "type": "integer", ++ "description": "Gets or sets the width.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Size": { ++ "type": "integer", ++ "description": "Gets or sets the size.", ++ "format": "int64" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class ImageInfo." ++ }, ++ "ImageOption": { ++ "type": "object", ++ "properties": { ++ "Type": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Gets or sets the type." ++ }, ++ "Limit": { ++ "type": "integer", ++ "description": "Gets or sets the limit.", ++ "format": "int32" ++ }, ++ "MinWidth": { ++ "type": "integer", ++ "description": "Gets or sets the minimum width.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "ImageOrientation": { ++ "enum": [ ++ "TopLeft", ++ "TopRight", ++ "BottomRight", ++ "BottomLeft", ++ "LeftTop", ++ "RightTop", ++ "RightBottom", ++ "LeftBottom" ++ ], ++ "type": "string" ++ }, ++ "ImageProviderInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets the name." ++ }, ++ "SupportedImages": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ }, ++ "description": "Gets the supported image types." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class ImageProviderInfo." ++ }, ++ "ImageResolution": { ++ "enum": [ ++ "MatchSource", ++ "P144", ++ "P240", ++ "P360", ++ "P480", ++ "P720", ++ "P1080", ++ "P1440", ++ "P2160" ++ ], ++ "type": "string", ++ "description": "Enum ImageResolution." ++ }, ++ "ImageSavingConvention": { ++ "enum": [ ++ "Legacy", ++ "Compatible" ++ ], ++ "type": "string" ++ }, ++ "ImageType": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "type": "string", ++ "description": "Enum ImageType." ++ }, ++ "InboundKeepAliveMessage": { ++ "type": "object", ++ "properties": { ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "KeepAlive", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Keep alive websocket messages." ++ }, ++ "InboundWebSocketMessage": { ++ "type": "object", ++ "oneOf": [ ++ { ++ "$ref": "#/components/schemas/ActivityLogEntryStartMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/ActivityLogEntryStopMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/InboundKeepAliveMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/ScheduledTasksInfoStartMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/ScheduledTasksInfoStopMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/SessionsStartMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/SessionsStopMessage" ++ } ++ ], ++ "description": "Represents the list of possible inbound websocket types", ++ "discriminator": { ++ "propertyName": "MessageType", ++ "mapping": { ++ "ActivityLogEntryStart": "#/components/schemas/ActivityLogEntryStartMessage", ++ "ActivityLogEntryStop": "#/components/schemas/ActivityLogEntryStopMessage", ++ "KeepAlive": "#/components/schemas/InboundKeepAliveMessage", ++ "ScheduledTasksInfoStart": "#/components/schemas/ScheduledTasksInfoStartMessage", ++ "ScheduledTasksInfoStop": "#/components/schemas/ScheduledTasksInfoStopMessage", ++ "SessionsStart": "#/components/schemas/SessionsStartMessage", ++ "SessionsStop": "#/components/schemas/SessionsStopMessage" ++ } ++ } ++ }, ++ "InstallationInfo": { ++ "type": "object", ++ "properties": { ++ "Guid": { ++ "type": "string", ++ "description": "Gets or sets the Id.", ++ "format": "uuid" ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "Version": { ++ "type": "string", ++ "description": "Gets or sets the version.", ++ "nullable": true ++ }, ++ "Changelog": { ++ "type": "string", ++ "description": "Gets or sets the changelog for this version.", ++ "nullable": true ++ }, ++ "SourceUrl": { ++ "type": "string", ++ "description": "Gets or sets the source URL.", ++ "nullable": true ++ }, ++ "Checksum": { ++ "type": "string", ++ "description": "Gets or sets a checksum for the binary.", ++ "nullable": true ++ }, ++ "PackageInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PackageInfo" ++ } ++ ], ++ "description": "Gets or sets package information for the installation.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class InstallationInfo." ++ }, ++ "IPlugin": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets the name of the plugin.", ++ "nullable": true, ++ "readOnly": true ++ }, ++ "Description": { ++ "type": "string", ++ "description": "Gets the Description.", ++ "nullable": true, ++ "readOnly": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets the unique id.", ++ "format": "uuid", ++ "readOnly": true ++ }, ++ "Version": { ++ "type": "string", ++ "description": "Gets the plugin version.", ++ "nullable": true, ++ "readOnly": true ++ }, ++ "AssemblyFilePath": { ++ "type": "string", ++ "description": "Gets the path to the assembly file.", ++ "nullable": true, ++ "readOnly": true ++ }, ++ "CanUninstall": { ++ "type": "boolean", ++ "description": "Gets a value indicating whether the plugin can be uninstalled.", ++ "readOnly": true ++ }, ++ "DataFolderPath": { ++ "type": "string", ++ "description": "Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.", ++ "nullable": true, ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines the MediaBrowser.Common.Plugins.IPlugin." ++ }, ++ "IsoType": { ++ "enum": [ ++ "Dvd", ++ "BluRay" ++ ], ++ "type": "string", ++ "description": "Enum IsoType." ++ }, ++ "ItemCounts": { ++ "type": "object", ++ "properties": { ++ "MovieCount": { ++ "type": "integer", ++ "description": "Gets or sets the movie count.", ++ "format": "int32" ++ }, ++ "SeriesCount": { ++ "type": "integer", ++ "description": "Gets or sets the series count.", ++ "format": "int32" ++ }, ++ "EpisodeCount": { ++ "type": "integer", ++ "description": "Gets or sets the episode count.", ++ "format": "int32" ++ }, ++ "ArtistCount": { ++ "type": "integer", ++ "description": "Gets or sets the artist count.", ++ "format": "int32" ++ }, ++ "ProgramCount": { ++ "type": "integer", ++ "description": "Gets or sets the program count.", ++ "format": "int32" ++ }, ++ "TrailerCount": { ++ "type": "integer", ++ "description": "Gets or sets the trailer count.", ++ "format": "int32" ++ }, ++ "SongCount": { ++ "type": "integer", ++ "description": "Gets or sets the song count.", ++ "format": "int32" ++ }, ++ "AlbumCount": { ++ "type": "integer", ++ "description": "Gets or sets the album count.", ++ "format": "int32" ++ }, ++ "MusicVideoCount": { ++ "type": "integer", ++ "description": "Gets or sets the music video count.", ++ "format": "int32" ++ }, ++ "BoxSetCount": { ++ "type": "integer", ++ "description": "Gets or sets the box set count.", ++ "format": "int32" ++ }, ++ "BookCount": { ++ "type": "integer", ++ "description": "Gets or sets the book count.", ++ "format": "int32" ++ }, ++ "ItemCount": { ++ "type": "integer", ++ "description": "Gets or sets the item count.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class LibrarySummary." ++ }, ++ "ItemFields": { ++ "enum": [ ++ "AirTime", ++ "CanDelete", ++ "CanDownload", ++ "ChannelInfo", ++ "Chapters", ++ "Trickplay", ++ "ChildCount", ++ "CumulativeRunTimeTicks", ++ "CustomRating", ++ "DateCreated", ++ "DateLastMediaAdded", ++ "DisplayPreferencesId", ++ "Etag", ++ "ExternalUrls", ++ "Genres", ++ "ItemCounts", ++ "MediaSourceCount", ++ "MediaSources", ++ "OriginalTitle", ++ "Overview", ++ "ParentId", ++ "Path", ++ "People", ++ "PlayAccess", ++ "ProductionLocations", ++ "ProviderIds", ++ "PrimaryImageAspectRatio", ++ "RecursiveItemCount", ++ "Settings", ++ "SeriesStudio", ++ "SortName", ++ "SpecialEpisodeNumbers", ++ "Studios", ++ "Taglines", ++ "Tags", ++ "RemoteTrailers", ++ "MediaStreams", ++ "SeasonUserData", ++ "DateLastRefreshed", ++ "DateLastSaved", ++ "RefreshState", ++ "ChannelImage", ++ "EnableMediaSourceDisplay", ++ "Width", ++ "Height", ++ "ExtraIds", ++ "LocalTrailerCount", ++ "IsHD", ++ "SpecialFeatureCount" ++ ], ++ "type": "string", ++ "description": "Used to control the data that gets attached to DtoBaseItems." ++ }, ++ "ItemFilter": { ++ "enum": [ ++ "IsFolder", ++ "IsNotFolder", ++ "IsUnplayed", ++ "IsPlayed", ++ "IsFavorite", ++ "IsResumable", ++ "Likes", ++ "Dislikes", ++ "IsFavoriteOrLikes" ++ ], ++ "type": "string", ++ "description": "Enum ItemFilter." ++ }, ++ "ItemSortBy": { ++ "enum": [ ++ "Default", ++ "AiredEpisodeOrder", ++ "Album", ++ "AlbumArtist", ++ "Artist", ++ "DateCreated", ++ "OfficialRating", ++ "DatePlayed", ++ "PremiereDate", ++ "StartDate", ++ "SortName", ++ "Name", ++ "Random", ++ "Runtime", ++ "CommunityRating", ++ "ProductionYear", ++ "PlayCount", ++ "CriticRating", ++ "IsFolder", ++ "IsUnplayed", ++ "IsPlayed", ++ "SeriesSortName", ++ "VideoBitRate", ++ "AirTime", ++ "Studio", ++ "IsFavoriteOrLiked", ++ "DateLastContentAdded", ++ "SeriesDatePlayed", ++ "ParentIndexNumber", ++ "IndexNumber" ++ ], ++ "type": "string", ++ "description": "These represent sort orders." ++ }, ++ "JoinGroupRequestDto": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets or sets the group identifier.", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class JoinGroupRequestDto." ++ }, ++ "KeepUntil": { ++ "enum": [ ++ "UntilDeleted", ++ "UntilSpaceNeeded", ++ "UntilWatched", ++ "UntilDate" ++ ], ++ "type": "string" ++ }, ++ "LibraryChangedMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/LibraryUpdateInfo" ++ } ++ ], ++ "description": "Class LibraryUpdateInfo.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "LibraryChanged", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Library changed message." ++ }, ++ "LibraryOptionInfoDto": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets name.", ++ "nullable": true ++ }, ++ "DefaultEnabled": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether default enabled." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Library option info dto." ++ }, ++ "LibraryOptions": { ++ "type": "object", ++ "properties": { ++ "Enabled": { ++ "type": "boolean" ++ }, ++ "EnablePhotos": { ++ "type": "boolean" ++ }, ++ "EnableRealtimeMonitor": { ++ "type": "boolean" ++ }, ++ "EnableLUFSScan": { ++ "type": "boolean" ++ }, ++ "EnableChapterImageExtraction": { ++ "type": "boolean" ++ }, ++ "ExtractChapterImagesDuringLibraryScan": { ++ "type": "boolean" ++ }, ++ "EnableTrickplayImageExtraction": { ++ "type": "boolean" ++ }, ++ "ExtractTrickplayImagesDuringLibraryScan": { ++ "type": "boolean" ++ }, ++ "PathInfos": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaPathInfo" ++ } ++ }, ++ "SaveLocalMetadata": { ++ "type": "boolean" ++ }, ++ "EnableInternetProviders": { ++ "type": "boolean", ++ "deprecated": true ++ }, ++ "EnableAutomaticSeriesGrouping": { ++ "type": "boolean" ++ }, ++ "EnableEmbeddedTitles": { ++ "type": "boolean" ++ }, ++ "EnableEmbeddedExtrasTitles": { ++ "type": "boolean" ++ }, ++ "EnableEmbeddedEpisodeInfos": { ++ "type": "boolean" ++ }, ++ "AutomaticRefreshIntervalDays": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "PreferredMetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the preferred metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "SeasonZeroDisplayName": { ++ "type": "string" ++ }, ++ "MetadataSavers": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "DisabledLocalMetadataReaders": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "LocalMetadataReaderOrder": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "DisabledSubtitleFetchers": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "SubtitleFetcherOrder": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "DisabledMediaSegmentProviders": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "MediaSegmentProviderOrder": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "SkipSubtitlesIfEmbeddedSubtitlesPresent": { ++ "type": "boolean" ++ }, ++ "SkipSubtitlesIfAudioTrackMatches": { ++ "type": "boolean" ++ }, ++ "SubtitleDownloadLanguages": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "RequirePerfectSubtitleMatch": { ++ "type": "boolean" ++ }, ++ "SaveSubtitlesWithMedia": { ++ "type": "boolean" ++ }, ++ "SaveLyricsWithMedia": { ++ "type": "boolean", ++ "default": false ++ }, ++ "SaveTrickplayWithMedia": { ++ "type": "boolean", ++ "default": false ++ }, ++ "DisabledLyricFetchers": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "LyricFetcherOrder": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "PreferNonstandardArtistsTag": { ++ "type": "boolean", ++ "default": false ++ }, ++ "UseCustomTagDelimiters": { ++ "type": "boolean", ++ "default": false ++ }, ++ "CustomTagDelimiters": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "DelimiterWhitelist": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "AutomaticallyAddToCollection": { ++ "type": "boolean" ++ }, ++ "AllowEmbeddedSubtitles": { ++ "enum": [ ++ "AllowAll", ++ "AllowText", ++ "AllowImage", ++ "AllowNone" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EmbeddedSubtitleOptions" ++ } ++ ], ++ "description": "An enum representing the options to disable embedded subs." ++ }, ++ "TypeOptions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TypeOptions" ++ } ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "LibraryOptionsResultDto": { ++ "type": "object", ++ "properties": { ++ "MetadataSavers": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LibraryOptionInfoDto" ++ }, ++ "description": "Gets or sets the metadata savers." ++ }, ++ "MetadataReaders": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LibraryOptionInfoDto" ++ }, ++ "description": "Gets or sets the metadata readers." ++ }, ++ "SubtitleFetchers": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LibraryOptionInfoDto" ++ }, ++ "description": "Gets or sets the subtitle fetchers." ++ }, ++ "LyricFetchers": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LibraryOptionInfoDto" ++ }, ++ "description": "Gets or sets the list of lyric fetchers." ++ }, ++ "MediaSegmentProviders": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LibraryOptionInfoDto" ++ }, ++ "description": "Gets or sets the list of MediaSegment Providers." ++ }, ++ "TypeOptions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LibraryTypeOptionsDto" ++ }, ++ "description": "Gets or sets the type options." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Library options result dto." ++ }, ++ "LibraryStorageDto": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the Library Id.", ++ "format": "uuid" ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name of the library." ++ }, ++ "Folders": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/FolderStorageDto" ++ }, ++ "description": "Gets or sets the storage informations about the folders used in a library." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Contains informations about a libraries storage informations." ++ }, ++ "LibraryTypeOptionsDto": { ++ "type": "object", ++ "properties": { ++ "Type": { ++ "type": "string", ++ "description": "Gets or sets the type.", ++ "nullable": true ++ }, ++ "MetadataFetchers": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LibraryOptionInfoDto" ++ }, ++ "description": "Gets or sets the metadata fetchers." ++ }, ++ "ImageFetchers": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LibraryOptionInfoDto" ++ }, ++ "description": "Gets or sets the image fetchers." ++ }, ++ "SupportedImageTypes": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageType" ++ }, ++ "description": "Gets or sets the supported image types." ++ }, ++ "DefaultImageOptions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageOption" ++ }, ++ "description": "Gets or sets the default image options." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Library type options dto." ++ }, ++ "LibraryUpdateInfo": { ++ "type": "object", ++ "properties": { ++ "FoldersAddedTo": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the folders added to." ++ }, ++ "FoldersRemovedFrom": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the folders removed from." ++ }, ++ "ItemsAdded": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the items added." ++ }, ++ "ItemsRemoved": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the items removed." ++ }, ++ "ItemsUpdated": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the items updated." ++ }, ++ "CollectionFolders": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "IsEmpty": { ++ "type": "boolean", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class LibraryUpdateInfo." ++ }, ++ "ListingsProviderInfo": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Type": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Username": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Password": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ListingsId": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ZipCode": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Country": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "nullable": true ++ }, ++ "EnabledTuners": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "EnableAllTuners": { ++ "type": "boolean" ++ }, ++ "NewsCategories": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "SportsCategories": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "KidsCategories": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "MovieCategories": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "ChannelMappings": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameValuePair" ++ }, ++ "nullable": true ++ }, ++ "MoviePrefix": { ++ "type": "string", ++ "nullable": true ++ }, ++ "PreferredLanguage": { ++ "type": "string", ++ "nullable": true ++ }, ++ "UserAgent": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "LiveStreamResponse": { ++ "type": "object", ++ "properties": { ++ "MediaSource": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaSourceInfo" ++ } ++ ] ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "LiveTvInfo": { ++ "type": "object", ++ "properties": { ++ "Services": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LiveTvServiceInfo" ++ }, ++ "description": "Gets or sets the services." ++ }, ++ "IsEnabled": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is enabled." ++ }, ++ "EnabledUsers": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the enabled users." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "LiveTvOptions": { ++ "type": "object", ++ "properties": { ++ "GuideDays": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "RecordingPath": { ++ "type": "string", ++ "nullable": true ++ }, ++ "MovieRecordingPath": { ++ "type": "string", ++ "nullable": true ++ }, ++ "SeriesRecordingPath": { ++ "type": "string", ++ "nullable": true ++ }, ++ "EnableRecordingSubfolders": { ++ "type": "boolean" ++ }, ++ "EnableOriginalAudioWithEncodedRecordings": { ++ "type": "boolean" ++ }, ++ "TunerHosts": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TunerHostInfo" ++ }, ++ "nullable": true ++ }, ++ "ListingProviders": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ListingsProviderInfo" ++ }, ++ "nullable": true ++ }, ++ "PrePaddingSeconds": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "PostPaddingSeconds": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "MediaLocationsCreated": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "RecordingPostProcessor": { ++ "type": "string", ++ "nullable": true ++ }, ++ "RecordingPostProcessorArguments": { ++ "type": "string", ++ "nullable": true ++ }, ++ "SaveRecordingNFO": { ++ "type": "boolean" ++ }, ++ "SaveRecordingImages": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "LiveTvServiceInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "HomePageUrl": { ++ "type": "string", ++ "description": "Gets or sets the home page URL.", ++ "nullable": true ++ }, ++ "Status": { ++ "enum": [ ++ "Ok", ++ "Unavailable" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/LiveTvServiceStatus" ++ } ++ ], ++ "description": "Gets or sets the status." ++ }, ++ "StatusMessage": { ++ "type": "string", ++ "description": "Gets or sets the status message.", ++ "nullable": true ++ }, ++ "Version": { ++ "type": "string", ++ "description": "Gets or sets the version.", ++ "nullable": true ++ }, ++ "HasUpdateAvailable": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance has update available." ++ }, ++ "IsVisible": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is visible." ++ }, ++ "Tuners": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class ServiceInfo." ++ }, ++ "LiveTvServiceStatus": { ++ "enum": [ ++ "Ok", ++ "Unavailable" ++ ], ++ "type": "string" ++ }, ++ "LocalizationOption": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Value": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "LocationType": { ++ "enum": [ ++ "FileSystem", ++ "Remote", ++ "Virtual", ++ "Offline" ++ ], ++ "type": "string", ++ "description": "Enum LocationType." ++ }, ++ "LogFile": { ++ "type": "object", ++ "properties": { ++ "DateCreated": { ++ "type": "string", ++ "description": "Gets or sets the date created.", ++ "format": "date-time" ++ }, ++ "DateModified": { ++ "type": "string", ++ "description": "Gets or sets the date modified.", ++ "format": "date-time" ++ }, ++ "Size": { ++ "type": "integer", ++ "description": "Gets or sets the size.", ++ "format": "int64" ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "LogLevel": { ++ "enum": [ ++ "Trace", ++ "Debug", ++ "Information", ++ "Warning", ++ "Error", ++ "Critical", ++ "None" ++ ], ++ "type": "string" ++ }, ++ "LyricDto": { ++ "type": "object", ++ "properties": { ++ "Metadata": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/LyricMetadata" ++ } ++ ], ++ "description": "Gets or sets Metadata for the lyrics." ++ }, ++ "Lyrics": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LyricLine" ++ }, ++ "description": "Gets or sets a collection of individual lyric lines." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "LyricResponse model." ++ }, ++ "LyricLine": { ++ "type": "object", ++ "properties": { ++ "Text": { ++ "type": "string", ++ "description": "Gets the text of this lyric line." ++ }, ++ "Start": { ++ "type": "integer", ++ "description": "Gets the start time in ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "Cues": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LyricLineCue" ++ }, ++ "description": "Gets the time-aligned cues for the song's lyrics.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Lyric model." ++ }, ++ "LyricLineCue": { ++ "type": "object", ++ "properties": { ++ "Position": { ++ "type": "integer", ++ "description": "Gets the start character index of the cue.", ++ "format": "int32" ++ }, ++ "EndPosition": { ++ "type": "integer", ++ "description": "Gets the end character index of the cue.", ++ "format": "int32" ++ }, ++ "Start": { ++ "type": "integer", ++ "description": "Gets the timestamp the lyric is synced to in ticks.", ++ "format": "int64" ++ }, ++ "End": { ++ "type": "integer", ++ "description": "Gets the end timestamp the lyric is synced to in ticks.", ++ "format": "int64", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "LyricLineCue model, holds information about the timing of words within a LyricLine." ++ }, ++ "LyricMetadata": { ++ "type": "object", ++ "properties": { ++ "Artist": { ++ "type": "string", ++ "description": "Gets or sets the song artist.", ++ "nullable": true ++ }, ++ "Album": { ++ "type": "string", ++ "description": "Gets or sets the album this song is on.", ++ "nullable": true ++ }, ++ "Title": { ++ "type": "string", ++ "description": "Gets or sets the title of the song.", ++ "nullable": true ++ }, ++ "Author": { ++ "type": "string", ++ "description": "Gets or sets the author of the lyric data.", ++ "nullable": true ++ }, ++ "Length": { ++ "type": "integer", ++ "description": "Gets or sets the length of the song in ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "By": { ++ "type": "string", ++ "description": "Gets or sets who the LRC file was created by.", ++ "nullable": true ++ }, ++ "Offset": { ++ "type": "integer", ++ "description": "Gets or sets the lyric offset compared to audio in ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "Creator": { ++ "type": "string", ++ "description": "Gets or sets the software used to create the LRC file.", ++ "nullable": true ++ }, ++ "Version": { ++ "type": "string", ++ "description": "Gets or sets the version of the creator used.", ++ "nullable": true ++ }, ++ "IsSynced": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this lyric is synced.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "LyricMetadata model." ++ }, ++ "MediaAttachment": { ++ "type": "object", ++ "properties": { ++ "Codec": { ++ "type": "string", ++ "description": "Gets or sets the codec.", ++ "nullable": true ++ }, ++ "CodecTag": { ++ "type": "string", ++ "description": "Gets or sets the codec tag.", ++ "nullable": true ++ }, ++ "Comment": { ++ "type": "string", ++ "description": "Gets or sets the comment.", ++ "nullable": true ++ }, ++ "Index": { ++ "type": "integer", ++ "description": "Gets or sets the index.", ++ "format": "int32" ++ }, ++ "FileName": { ++ "type": "string", ++ "description": "Gets or sets the filename.", ++ "nullable": true ++ }, ++ "MimeType": { ++ "type": "string", ++ "description": "Gets or sets the MIME type.", ++ "nullable": true ++ }, ++ "DeliveryUrl": { ++ "type": "string", ++ "description": "Gets or sets the delivery URL.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class MediaAttachment." ++ }, ++ "MediaPathDto": { ++ "required": [ ++ "Name" ++ ], ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name of the library." ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path to add.", ++ "nullable": true ++ }, ++ "PathInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaPathInfo" ++ } ++ ], ++ "description": "Gets or sets the path info.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Media Path dto." ++ }, ++ "MediaPathInfo": { ++ "type": "object", ++ "properties": { ++ "Path": { ++ "type": "string" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "MediaProtocol": { ++ "enum": [ ++ "File", ++ "Http", ++ "Rtmp", ++ "Rtsp", ++ "Udp", ++ "Rtp", ++ "Ftp" ++ ], ++ "type": "string" ++ }, ++ "MediaSegmentDto": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the id of the media segment.", ++ "format": "uuid" ++ }, ++ "ItemId": { ++ "type": "string", ++ "description": "Gets or sets the id of the associated item.", ++ "format": "uuid" ++ }, ++ "Type": { ++ "enum": [ ++ "Unknown", ++ "Commercial", ++ "Preview", ++ "Recap", ++ "Outro", ++ "Intro" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaSegmentType" ++ } ++ ], ++ "description": "Gets or sets the type of content this segment defines.", ++ "default": "Unknown" ++ }, ++ "StartTicks": { ++ "type": "integer", ++ "description": "Gets or sets the start of the segment.", ++ "format": "int64" ++ }, ++ "EndTicks": { ++ "type": "integer", ++ "description": "Gets or sets the end of the segment.", ++ "format": "int64" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Api model for MediaSegment's." ++ }, ++ "MediaSegmentDtoQueryResult": { ++ "type": "object", ++ "properties": { ++ "Items": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaSegmentDto" ++ }, ++ "description": "Gets or sets the items." ++ }, ++ "TotalRecordCount": { ++ "type": "integer", ++ "description": "Gets or sets the total number of records available.", ++ "format": "int32" ++ }, ++ "StartIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the first record in Items.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Query result container." ++ }, ++ "MediaSegmentType": { ++ "enum": [ ++ "Unknown", ++ "Commercial", ++ "Preview", ++ "Recap", ++ "Outro", ++ "Intro" ++ ], ++ "type": "string", ++ "description": "Defines the types of content an individual Jellyfin.Database.Implementations.Entities.MediaSegment represents." ++ }, ++ "MediaSourceInfo": { ++ "type": "object", ++ "properties": { ++ "Protocol": { ++ "enum": [ ++ "File", ++ "Http", ++ "Rtmp", ++ "Rtsp", ++ "Udp", ++ "Rtp", ++ "Ftp" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaProtocol" ++ } ++ ] ++ }, ++ "Id": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "nullable": true ++ }, ++ "EncoderPath": { ++ "type": "string", ++ "nullable": true ++ }, ++ "EncoderProtocol": { ++ "enum": [ ++ "File", ++ "Http", ++ "Rtmp", ++ "Rtsp", ++ "Udp", ++ "Rtp", ++ "Ftp" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaProtocol" ++ } ++ ], ++ "nullable": true ++ }, ++ "Type": { ++ "enum": [ ++ "Default", ++ "Grouping", ++ "Placeholder" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaSourceType" ++ } ++ ] ++ }, ++ "Container": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Size": { ++ "type": "integer", ++ "format": "int64", ++ "nullable": true ++ }, ++ "Name": { ++ "type": "string", ++ "nullable": true ++ }, ++ "IsRemote": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the media is remote.\r\nDifferentiate internet url vs local network." ++ }, ++ "ETag": { ++ "type": "string", ++ "nullable": true ++ }, ++ "RunTimeTicks": { ++ "type": "integer", ++ "format": "int64", ++ "nullable": true ++ }, ++ "ReadAtNativeFramerate": { ++ "type": "boolean" ++ }, ++ "IgnoreDts": { ++ "type": "boolean" ++ }, ++ "IgnoreIndex": { ++ "type": "boolean" ++ }, ++ "GenPtsInput": { ++ "type": "boolean" ++ }, ++ "SupportsTranscoding": { ++ "type": "boolean" ++ }, ++ "SupportsDirectStream": { ++ "type": "boolean" ++ }, ++ "SupportsDirectPlay": { ++ "type": "boolean" ++ }, ++ "IsInfiniteStream": { ++ "type": "boolean" ++ }, ++ "UseMostCompatibleTranscodingProfile": { ++ "type": "boolean", ++ "default": false ++ }, ++ "RequiresOpening": { ++ "type": "boolean" ++ }, ++ "OpenToken": { ++ "type": "string", ++ "nullable": true ++ }, ++ "RequiresClosing": { ++ "type": "boolean" ++ }, ++ "LiveStreamId": { ++ "type": "string", ++ "nullable": true ++ }, ++ "BufferMs": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "RequiresLooping": { ++ "type": "boolean" ++ }, ++ "SupportsProbing": { ++ "type": "boolean" ++ }, ++ "VideoType": { ++ "enum": [ ++ "VideoFile", ++ "Iso", ++ "Dvd", ++ "BluRay" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/VideoType" ++ } ++ ], ++ "nullable": true ++ }, ++ "IsoType": { ++ "enum": [ ++ "Dvd", ++ "BluRay" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/IsoType" ++ } ++ ], ++ "nullable": true ++ }, ++ "Video3DFormat": { ++ "enum": [ ++ "HalfSideBySide", ++ "FullSideBySide", ++ "FullTopAndBottom", ++ "HalfTopAndBottom", ++ "MVC" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/Video3DFormat" ++ } ++ ], ++ "nullable": true ++ }, ++ "MediaStreams": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaStream" ++ }, ++ "nullable": true ++ }, ++ "MediaAttachments": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaAttachment" ++ }, ++ "nullable": true ++ }, ++ "Formats": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "Bitrate": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "FallbackMaxStreamingBitrate": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Timestamp": { ++ "enum": [ ++ "None", ++ "Zero", ++ "Valid" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TransportStreamTimestamp" ++ } ++ ], ++ "nullable": true ++ }, ++ "RequiredHttpHeaders": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "nullable": true ++ }, ++ "TranscodingUrl": { ++ "type": "string", ++ "nullable": true ++ }, ++ "TranscodingSubProtocol": { ++ "enum": [ ++ "http", ++ "hls" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaStreamProtocol" ++ } ++ ], ++ "description": "Media streaming protocol.\r\nLowercase for backwards compatibility." ++ }, ++ "TranscodingContainer": { ++ "type": "string", ++ "nullable": true ++ }, ++ "AnalyzeDurationMs": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "DefaultAudioStreamIndex": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "DefaultSubtitleStreamIndex": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "HasSegments": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "MediaSourceType": { ++ "enum": [ ++ "Default", ++ "Grouping", ++ "Placeholder" ++ ], ++ "type": "string" ++ }, ++ "MediaStream": { ++ "type": "object", ++ "properties": { ++ "Codec": { ++ "type": "string", ++ "description": "Gets or sets the codec.", ++ "nullable": true ++ }, ++ "CodecTag": { ++ "type": "string", ++ "description": "Gets or sets the codec tag.", ++ "nullable": true ++ }, ++ "Language": { ++ "type": "string", ++ "description": "Gets or sets the language.", ++ "nullable": true ++ }, ++ "ColorRange": { ++ "type": "string", ++ "description": "Gets or sets the color range.", ++ "nullable": true ++ }, ++ "ColorSpace": { ++ "type": "string", ++ "description": "Gets or sets the color space.", ++ "nullable": true ++ }, ++ "ColorTransfer": { ++ "type": "string", ++ "description": "Gets or sets the color transfer.", ++ "nullable": true ++ }, ++ "ColorPrimaries": { ++ "type": "string", ++ "description": "Gets or sets the color primaries.", ++ "nullable": true ++ }, ++ "DvVersionMajor": { ++ "type": "integer", ++ "description": "Gets or sets the Dolby Vision version major.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "DvVersionMinor": { ++ "type": "integer", ++ "description": "Gets or sets the Dolby Vision version minor.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "DvProfile": { ++ "type": "integer", ++ "description": "Gets or sets the Dolby Vision profile.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "DvLevel": { ++ "type": "integer", ++ "description": "Gets or sets the Dolby Vision level.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "RpuPresentFlag": { ++ "type": "integer", ++ "description": "Gets or sets the Dolby Vision rpu present flag.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ElPresentFlag": { ++ "type": "integer", ++ "description": "Gets or sets the Dolby Vision el present flag.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "BlPresentFlag": { ++ "type": "integer", ++ "description": "Gets or sets the Dolby Vision bl present flag.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "DvBlSignalCompatibilityId": { ++ "type": "integer", ++ "description": "Gets or sets the Dolby Vision bl signal compatibility id.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Rotation": { ++ "type": "integer", ++ "description": "Gets or sets the Rotation in degrees.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Comment": { ++ "type": "string", ++ "description": "Gets or sets the comment.", ++ "nullable": true ++ }, ++ "TimeBase": { ++ "type": "string", ++ "description": "Gets or sets the time base.", ++ "nullable": true ++ }, ++ "CodecTimeBase": { ++ "type": "string", ++ "description": "Gets or sets the codec time base.", ++ "nullable": true ++ }, ++ "Title": { ++ "type": "string", ++ "description": "Gets or sets the title.", ++ "nullable": true ++ }, ++ "Hdr10PlusPresentFlag": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "VideoRange": { ++ "enum": [ ++ "Unknown", ++ "SDR", ++ "HDR" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/VideoRange" ++ } ++ ], ++ "description": "Gets the video range.", ++ "default": "Unknown", ++ "readOnly": true ++ }, ++ "VideoRangeType": { ++ "enum": [ ++ "Unknown", ++ "SDR", ++ "HDR10", ++ "HLG", ++ "DOVI", ++ "DOVIWithHDR10", ++ "DOVIWithHLG", ++ "DOVIWithSDR", ++ "DOVIWithEL", ++ "DOVIWithHDR10Plus", ++ "DOVIWithELHDR10Plus", ++ "DOVIInvalid", ++ "HDR10Plus" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/VideoRangeType" ++ } ++ ], ++ "description": "Gets the video range type.", ++ "default": "Unknown", ++ "readOnly": true ++ }, ++ "VideoDoViTitle": { ++ "type": "string", ++ "description": "Gets the video dovi title.", ++ "nullable": true, ++ "readOnly": true ++ }, ++ "AudioSpatialFormat": { ++ "enum": [ ++ "None", ++ "DolbyAtmos", ++ "DTSX" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/AudioSpatialFormat" ++ } ++ ], ++ "description": "Gets the audio spatial format.", ++ "default": "None", ++ "readOnly": true ++ }, ++ "LocalizedUndefined": { ++ "type": "string", ++ "nullable": true ++ }, ++ "LocalizedDefault": { ++ "type": "string", ++ "nullable": true ++ }, ++ "LocalizedForced": { ++ "type": "string", ++ "nullable": true ++ }, ++ "LocalizedExternal": { ++ "type": "string", ++ "nullable": true ++ }, ++ "LocalizedHearingImpaired": { ++ "type": "string", ++ "nullable": true ++ }, ++ "DisplayTitle": { ++ "type": "string", ++ "nullable": true, ++ "readOnly": true ++ }, ++ "NalLengthSize": { ++ "type": "string", ++ "nullable": true ++ }, ++ "IsInterlaced": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is interlaced." ++ }, ++ "IsAVC": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "ChannelLayout": { ++ "type": "string", ++ "description": "Gets or sets the channel layout.", ++ "nullable": true ++ }, ++ "BitRate": { ++ "type": "integer", ++ "description": "Gets or sets the bit rate.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "BitDepth": { ++ "type": "integer", ++ "description": "Gets or sets the bit depth.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "RefFrames": { ++ "type": "integer", ++ "description": "Gets or sets the reference frames.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PacketLength": { ++ "type": "integer", ++ "description": "Gets or sets the length of the packet.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Channels": { ++ "type": "integer", ++ "description": "Gets or sets the channels.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SampleRate": { ++ "type": "integer", ++ "description": "Gets or sets the sample rate.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IsDefault": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is default." ++ }, ++ "IsForced": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is forced." ++ }, ++ "IsHearingImpaired": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is for the hearing impaired." ++ }, ++ "Height": { ++ "type": "integer", ++ "description": "Gets or sets the height.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Width": { ++ "type": "integer", ++ "description": "Gets or sets the width.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "AverageFrameRate": { ++ "type": "number", ++ "description": "Gets or sets the average frame rate.", ++ "format": "float", ++ "nullable": true ++ }, ++ "RealFrameRate": { ++ "type": "number", ++ "description": "Gets or sets the real frame rate.", ++ "format": "float", ++ "nullable": true ++ }, ++ "ReferenceFrameRate": { ++ "type": "number", ++ "description": "Gets the framerate used as reference.\r\nPrefer AverageFrameRate, if that is null or an unrealistic value\r\nthen fallback to RealFrameRate.", ++ "format": "float", ++ "nullable": true, ++ "readOnly": true ++ }, ++ "Profile": { ++ "type": "string", ++ "description": "Gets or sets the profile.", ++ "nullable": true ++ }, ++ "Type": { ++ "enum": [ ++ "Audio", ++ "Video", ++ "Subtitle", ++ "EmbeddedImage", ++ "Data", ++ "Lyric" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaStreamType" ++ } ++ ], ++ "description": "Gets or sets the type." ++ }, ++ "AspectRatio": { ++ "type": "string", ++ "description": "Gets or sets the aspect ratio.", ++ "nullable": true ++ }, ++ "Index": { ++ "type": "integer", ++ "description": "Gets or sets the index.", ++ "format": "int32" ++ }, ++ "Score": { ++ "type": "integer", ++ "description": "Gets or sets the score.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IsExternal": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is external." ++ }, ++ "DeliveryMethod": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ], ++ "description": "Gets or sets the method.", ++ "nullable": true ++ }, ++ "DeliveryUrl": { ++ "type": "string", ++ "description": "Gets or sets the delivery URL.", ++ "nullable": true ++ }, ++ "IsExternalUrl": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is external URL.", ++ "nullable": true ++ }, ++ "IsTextSubtitleStream": { ++ "type": "boolean", ++ "readOnly": true ++ }, ++ "SupportsExternalStream": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [supports external stream]." ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the filename.", ++ "nullable": true ++ }, ++ "PixelFormat": { ++ "type": "string", ++ "description": "Gets or sets the pixel format.", ++ "nullable": true ++ }, ++ "Level": { ++ "type": "number", ++ "description": "Gets or sets the level.", ++ "format": "double", ++ "nullable": true ++ }, ++ "IsAnamorphic": { ++ "type": "boolean", ++ "description": "Gets or sets whether this instance is anamorphic.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class MediaStream." ++ }, ++ "MediaStreamProtocol": { ++ "enum": [ ++ "http", ++ "hls" ++ ], ++ "type": "string", ++ "description": "Media streaming protocol.\r\nLowercase for backwards compatibility." ++ }, ++ "MediaStreamType": { ++ "enum": [ ++ "Audio", ++ "Video", ++ "Subtitle", ++ "EmbeddedImage", ++ "Data", ++ "Lyric" ++ ], ++ "type": "string", ++ "description": "Enum MediaStreamType." ++ }, ++ "MediaType": { ++ "enum": [ ++ "Unknown", ++ "Video", ++ "Audio", ++ "Photo", ++ "Book" ++ ], ++ "type": "string", ++ "description": "Media types." ++ }, ++ "MediaUpdateInfoDto": { ++ "type": "object", ++ "properties": { ++ "Updates": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaUpdateInfoPathDto" ++ }, ++ "description": "Gets or sets the list of updates." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Media Update Info Dto." ++ }, ++ "MediaUpdateInfoPathDto": { ++ "type": "object", ++ "properties": { ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets media path.", ++ "nullable": true ++ }, ++ "UpdateType": { ++ "type": "string", ++ "description": "Gets or sets media update type.\r\nCreated, Modified, Deleted.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The media update info path." ++ }, ++ "MediaUrl": { ++ "type": "object", ++ "properties": { ++ "Url": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Name": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "MessageCommand": { ++ "required": [ ++ "Text" ++ ], ++ "type": "object", ++ "properties": { ++ "Header": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Text": { ++ "type": "string" ++ }, ++ "TimeoutMs": { ++ "type": "integer", ++ "format": "int64", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "MetadataConfiguration": { ++ "type": "object", ++ "properties": { ++ "UseFileCreationTimeForDateAdded": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "MetadataEditorInfo": { ++ "type": "object", ++ "properties": { ++ "ParentalRatingOptions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ParentalRating" ++ }, ++ "description": "Gets or sets the parental rating options." ++ }, ++ "Countries": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CountryInfo" ++ }, ++ "description": "Gets or sets the countries." ++ }, ++ "Cultures": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CultureDto" ++ }, ++ "description": "Gets or sets the cultures." ++ }, ++ "ExternalIdInfos": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ExternalIdInfo" ++ }, ++ "description": "Gets or sets the external id infos." ++ }, ++ "ContentType": { ++ "enum": [ ++ "unknown", ++ "movies", ++ "tvshows", ++ "music", ++ "musicvideos", ++ "trailers", ++ "homevideos", ++ "boxsets", ++ "books", ++ "photos", ++ "livetv", ++ "playlists", ++ "folders" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CollectionType" ++ } ++ ], ++ "description": "Gets or sets the content type.", ++ "nullable": true ++ }, ++ "ContentTypeOptions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameValuePair" ++ }, ++ "description": "Gets or sets the content type options." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "A class representing metadata editor information." ++ }, ++ "MetadataField": { ++ "enum": [ ++ "Cast", ++ "Genres", ++ "ProductionLocations", ++ "Studios", ++ "Tags", ++ "Name", ++ "Overview", ++ "Runtime", ++ "OfficialRating" ++ ], ++ "type": "string", ++ "description": "Enum MetadataFields." ++ }, ++ "MetadataOptions": { ++ "type": "object", ++ "properties": { ++ "ItemType": { ++ "type": "string", ++ "nullable": true ++ }, ++ "DisabledMetadataSavers": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "LocalMetadataReaderOrder": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "DisabledMetadataFetchers": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "MetadataFetcherOrder": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "DisabledImageFetchers": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "ImageFetcherOrder": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class MetadataOptions." ++ }, ++ "MetadataRefreshMode": { ++ "enum": [ ++ "None", ++ "ValidationOnly", ++ "Default", ++ "FullRefresh" ++ ], ++ "type": "string" ++ }, ++ "MovePlaylistItemRequestDto": { ++ "type": "object", ++ "properties": { ++ "PlaylistItemId": { ++ "type": "string", ++ "description": "Gets or sets the playlist identifier of the item.", ++ "format": "uuid" ++ }, ++ "NewIndex": { ++ "type": "integer", ++ "description": "Gets or sets the new position.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class MovePlaylistItemRequestDto." ++ }, ++ "MovieInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "description": "Gets or sets the original title.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "MetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "Year": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsAutomated": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "MovieInfoRemoteSearchQuery": { ++ "type": "object", ++ "properties": { ++ "SearchInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MovieInfo" ++ } ++ ], ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "SearchProviderName": { ++ "type": "string", ++ "description": "Gets or sets the provider name to search within if set.", ++ "nullable": true ++ }, ++ "IncludeDisabledProviders": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether disabled providers should be included." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "MusicVideoInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "description": "Gets or sets the original title.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "MetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "Year": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsAutomated": { ++ "type": "boolean" ++ }, ++ "Artists": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "MusicVideoInfoRemoteSearchQuery": { ++ "type": "object", ++ "properties": { ++ "SearchInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MusicVideoInfo" ++ } ++ ], ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "SearchProviderName": { ++ "type": "string", ++ "description": "Gets or sets the provider name to search within if set.", ++ "nullable": true ++ }, ++ "IncludeDisabledProviders": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether disabled providers should be included." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "NameGuidPair": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "NameIdPair": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the identifier.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "NameValuePair": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "Value": { ++ "type": "string", ++ "description": "Gets or sets the value.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "NetworkConfiguration": { ++ "type": "object", ++ "properties": { ++ "BaseUrl": { ++ "type": "string", ++ "description": "Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at." ++ }, ++ "EnableHttps": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to use HTTPS." ++ }, ++ "RequireHttps": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the server should force connections over HTTPS." ++ }, ++ "CertificatePath": { ++ "type": "string", ++ "description": "Gets or sets the filesystem path of an X.509 certificate to use for SSL." ++ }, ++ "CertificatePassword": { ++ "type": "string", ++ "description": "Gets or sets the password required to access the X.509 certificate data in the file specified by MediaBrowser.Common.Net.NetworkConfiguration.CertificatePath." ++ }, ++ "InternalHttpPort": { ++ "type": "integer", ++ "description": "Gets or sets the internal HTTP server port.", ++ "format": "int32" ++ }, ++ "InternalHttpsPort": { ++ "type": "integer", ++ "description": "Gets or sets the internal HTTPS server port.", ++ "format": "int32" ++ }, ++ "PublicHttpPort": { ++ "type": "integer", ++ "description": "Gets or sets the public HTTP port.", ++ "format": "int32" ++ }, ++ "PublicHttpsPort": { ++ "type": "integer", ++ "description": "Gets or sets the public HTTPS port.", ++ "format": "int32" ++ }, ++ "AutoDiscovery": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether Autodiscovery is enabled." ++ }, ++ "EnableUPnP": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to enable automatic port forwarding.", ++ "deprecated": true ++ }, ++ "EnableIPv4": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether IPv6 is enabled." ++ }, ++ "EnableIPv6": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether IPv6 is enabled." ++ }, ++ "EnableRemoteAccess": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether access from outside of the LAN is permitted." ++ }, ++ "LocalNetworkSubnets": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the subnets that are deemed to make up the LAN." ++ }, ++ "LocalNetworkAddresses": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used." ++ }, ++ "KnownProxies": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the known proxies." ++ }, ++ "IgnoreVirtualInterfaces": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether address names that match MediaBrowser.Common.Net.NetworkConfiguration.VirtualInterfaceNames should be ignored for the purposes of binding." ++ }, ++ "VirtualInterfaceNames": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. ." ++ }, ++ "EnablePublishedServerUriByRequest": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the published server uri is based on information in HTTP requests." ++ }, ++ "PublishedServerUriBySubnet": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the PublishedServerUriBySubnet\r\nGets or sets PublishedServerUri to advertise for specific subnets." ++ }, ++ "RemoteIPFilter": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the filter for remote IP connectivity. Used in conjunction with ." ++ }, ++ "IsRemoteIPFilterBlacklist": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines the MediaBrowser.Common.Net.NetworkConfiguration." ++ }, ++ "NewGroupRequestDto": { ++ "type": "object", ++ "properties": { ++ "GroupName": { ++ "type": "string", ++ "description": "Gets or sets the group name." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class NewGroupRequestDto." ++ }, ++ "NextItemRequestDto": { ++ "type": "object", ++ "properties": { ++ "PlaylistItemId": { ++ "type": "string", ++ "description": "Gets or sets the playing item identifier.", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class NextItemRequestDto." ++ }, ++ "OpenLiveStreamDto": { ++ "type": "object", ++ "properties": { ++ "OpenToken": { ++ "type": "string", ++ "description": "Gets or sets the open token.", ++ "nullable": true ++ }, ++ "UserId": { ++ "type": "string", ++ "description": "Gets or sets the user id.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "PlaySessionId": { ++ "type": "string", ++ "description": "Gets or sets the play session id.", ++ "nullable": true ++ }, ++ "MaxStreamingBitrate": { ++ "type": "integer", ++ "description": "Gets or sets the max streaming bitrate.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "StartTimeTicks": { ++ "type": "integer", ++ "description": "Gets or sets the start time in ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "AudioStreamIndex": { ++ "type": "integer", ++ "description": "Gets or sets the audio stream index.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SubtitleStreamIndex": { ++ "type": "integer", ++ "description": "Gets or sets the subtitle stream index.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MaxAudioChannels": { ++ "type": "integer", ++ "description": "Gets or sets the max audio channels.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "description": "Gets or sets the item id.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "EnableDirectPlay": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to enable direct play.", ++ "nullable": true ++ }, ++ "EnableDirectStream": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to enable direct stream.", ++ "nullable": true ++ }, ++ "AlwaysBurnInSubtitleWhenTranscoding": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether always burn in subtitles when transcoding.", ++ "nullable": true ++ }, ++ "DeviceProfile": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DeviceProfile" ++ } ++ ], ++ "description": "A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.\r\n
\r\nSpecifically, it defines the supported containers and\r\ncodecs (video and/or audio, including codec profiles and levels)\r\nthe device is able to direct play (without transcoding or remuxing),\r\nas well as which containers/codecs to transcode to in case it isn't.", ++ "nullable": true ++ }, ++ "DirectPlayProtocols": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaProtocol" ++ }, ++ "description": "Gets or sets the device play protocols." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Open live stream dto." ++ }, ++ "OutboundKeepAliveMessage": { ++ "type": "object", ++ "properties": { ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "KeepAlive", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Keep alive websocket messages." ++ }, ++ "OutboundWebSocketMessage": { ++ "type": "object", ++ "oneOf": [ ++ { ++ "$ref": "#/components/schemas/ActivityLogEntryMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/ForceKeepAliveMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/GeneralCommandMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/LibraryChangedMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/OutboundKeepAliveMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/PlayMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/PlaystateMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/PluginInstallationCancelledMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/PluginInstallationCompletedMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/PluginInstallationFailedMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/PluginInstallingMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/PluginUninstalledMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/RefreshProgressMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/RestartRequiredMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/ScheduledTaskEndedMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/ScheduledTasksInfoMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/SeriesTimerCancelledMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/SeriesTimerCreatedMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/ServerRestartingMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/ServerShuttingDownMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/SessionsMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/SyncPlayCommandMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/TimerCancelledMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/TimerCreatedMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/UserDataChangedMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/UserDeletedMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/UserUpdatedMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/SyncPlayGroupUpdateMessage" ++ } ++ ], ++ "description": "Represents the list of possible outbound websocket types", ++ "discriminator": { ++ "propertyName": "MessageType", ++ "mapping": { ++ "ActivityLogEntry": "#/components/schemas/ActivityLogEntryMessage", ++ "ForceKeepAlive": "#/components/schemas/ForceKeepAliveMessage", ++ "GeneralCommand": "#/components/schemas/GeneralCommandMessage", ++ "LibraryChanged": "#/components/schemas/LibraryChangedMessage", ++ "KeepAlive": "#/components/schemas/OutboundKeepAliveMessage", ++ "Play": "#/components/schemas/PlayMessage", ++ "Playstate": "#/components/schemas/PlaystateMessage", ++ "PackageInstallationCancelled": "#/components/schemas/PluginInstallationCancelledMessage", ++ "PackageInstallationCompleted": "#/components/schemas/PluginInstallationCompletedMessage", ++ "PackageInstallationFailed": "#/components/schemas/PluginInstallationFailedMessage", ++ "PackageInstalling": "#/components/schemas/PluginInstallingMessage", ++ "PackageUninstalled": "#/components/schemas/PluginUninstalledMessage", ++ "RefreshProgress": "#/components/schemas/RefreshProgressMessage", ++ "RestartRequired": "#/components/schemas/RestartRequiredMessage", ++ "ScheduledTaskEnded": "#/components/schemas/ScheduledTaskEndedMessage", ++ "ScheduledTasksInfo": "#/components/schemas/ScheduledTasksInfoMessage", ++ "SeriesTimerCancelled": "#/components/schemas/SeriesTimerCancelledMessage", ++ "SeriesTimerCreated": "#/components/schemas/SeriesTimerCreatedMessage", ++ "ServerRestarting": "#/components/schemas/ServerRestartingMessage", ++ "ServerShuttingDown": "#/components/schemas/ServerShuttingDownMessage", ++ "Sessions": "#/components/schemas/SessionsMessage", ++ "SyncPlayCommand": "#/components/schemas/SyncPlayCommandMessage", ++ "TimerCancelled": "#/components/schemas/TimerCancelledMessage", ++ "TimerCreated": "#/components/schemas/TimerCreatedMessage", ++ "UserDataChanged": "#/components/schemas/UserDataChangedMessage", ++ "UserDeleted": "#/components/schemas/UserDeletedMessage", ++ "UserUpdated": "#/components/schemas/UserUpdatedMessage", ++ "SyncPlayGroupUpdate": "#/components/schemas/SyncPlayGroupUpdateMessage" ++ } ++ } ++ }, ++ "PackageInfo": { ++ "type": "object", ++ "properties": { ++ "name": { ++ "type": "string", ++ "description": "Gets or sets the name." ++ }, ++ "description": { ++ "type": "string", ++ "description": "Gets or sets a long description of the plugin containing features or helpful explanations." ++ }, ++ "overview": { ++ "type": "string", ++ "description": "Gets or sets a short overview of what the plugin does." ++ }, ++ "owner": { ++ "type": "string", ++ "description": "Gets or sets the owner." ++ }, ++ "category": { ++ "type": "string", ++ "description": "Gets or sets the category." ++ }, ++ "guid": { ++ "type": "string", ++ "description": "Gets or sets the guid of the assembly associated with this plugin.\r\nThis is used to identify the proper item for automatic updates.", ++ "format": "uuid" ++ }, ++ "versions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/VersionInfo" ++ }, ++ "description": "Gets or sets the versions." ++ }, ++ "imageUrl": { ++ "type": "string", ++ "description": "Gets or sets the image url for the package.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class PackageInfo." ++ }, ++ "ParentalRating": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name." ++ }, ++ "Value": { ++ "type": "integer", ++ "description": "Gets or sets the value.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "RatingScore": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ParentalRatingScore" ++ } ++ ], ++ "description": "Gets or sets the rating score.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class ParentalRating." ++ }, ++ "ParentalRatingScore": { ++ "type": "object", ++ "properties": { ++ "score": { ++ "type": "integer", ++ "description": "Gets or sets the score.", ++ "format": "int32" ++ }, ++ "subScore": { ++ "type": "integer", ++ "description": "Gets or sets the sub score.", ++ "format": "int32", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "A class representing an parental rating score." ++ }, ++ "PathSubstitution": { ++ "type": "object", ++ "properties": { ++ "From": { ++ "type": "string", ++ "description": "Gets or sets the value to substitute." ++ }, ++ "To": { ++ "type": "string", ++ "description": "Gets or sets the value to substitution with." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines the MediaBrowser.Model.Configuration.PathSubstitution." ++ }, ++ "PersonKind": { ++ "enum": [ ++ "Unknown", ++ "Actor", ++ "Director", ++ "Composer", ++ "Writer", ++ "GuestStar", ++ "Producer", ++ "Conductor", ++ "Lyricist", ++ "Arranger", ++ "Engineer", ++ "Mixer", ++ "Remixer", ++ "Creator", ++ "Artist", ++ "AlbumArtist", ++ "Author", ++ "Illustrator", ++ "Penciller", ++ "Inker", ++ "Colorist", ++ "Letterer", ++ "CoverArtist", ++ "Editor", ++ "Translator" ++ ], ++ "type": "string", ++ "description": "The person kind." ++ }, ++ "PersonLookupInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "description": "Gets or sets the original title.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "MetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "Year": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsAutomated": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "PersonLookupInfoRemoteSearchQuery": { ++ "type": "object", ++ "properties": { ++ "SearchInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PersonLookupInfo" ++ } ++ ], ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "SearchProviderName": { ++ "type": "string", ++ "description": "Gets or sets the provider name to search within if set.", ++ "nullable": true ++ }, ++ "IncludeDisabledProviders": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether disabled providers should be included." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "PingRequestDto": { ++ "type": "object", ++ "properties": { ++ "Ping": { ++ "type": "integer", ++ "description": "Gets or sets the ping time.", ++ "format": "int64" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class PingRequestDto." ++ }, ++ "PinRedeemResult": { ++ "type": "object", ++ "properties": { ++ "Success": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Users.PinRedeemResult is success." ++ }, ++ "UsersReset": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the users reset." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "PlayAccess": { ++ "enum": [ ++ "Full", ++ "None" ++ ], ++ "type": "string" ++ }, ++ "PlaybackErrorCode": { ++ "enum": [ ++ "NotAllowed", ++ "NoCompatibleStream", ++ "RateLimitExceeded" ++ ], ++ "type": "string" ++ }, ++ "PlaybackInfoDto": { ++ "type": "object", ++ "properties": { ++ "UserId": { ++ "type": "string", ++ "description": "Gets or sets the playback userId.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "MaxStreamingBitrate": { ++ "type": "integer", ++ "description": "Gets or sets the max streaming bitrate.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "StartTimeTicks": { ++ "type": "integer", ++ "description": "Gets or sets the start time in ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "AudioStreamIndex": { ++ "type": "integer", ++ "description": "Gets or sets the audio stream index.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SubtitleStreamIndex": { ++ "type": "integer", ++ "description": "Gets or sets the subtitle stream index.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MaxAudioChannels": { ++ "type": "integer", ++ "description": "Gets or sets the max audio channels.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MediaSourceId": { ++ "type": "string", ++ "description": "Gets or sets the media source id.", ++ "nullable": true ++ }, ++ "LiveStreamId": { ++ "type": "string", ++ "description": "Gets or sets the live stream id.", ++ "nullable": true ++ }, ++ "DeviceProfile": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DeviceProfile" ++ } ++ ], ++ "description": "A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.\r\n
\r\nSpecifically, it defines the supported containers and\r\ncodecs (video and/or audio, including codec profiles and levels)\r\nthe device is able to direct play (without transcoding or remuxing),\r\nas well as which containers/codecs to transcode to in case it isn't.", ++ "nullable": true ++ }, ++ "EnableDirectPlay": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to enable direct play.", ++ "nullable": true ++ }, ++ "EnableDirectStream": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to enable direct stream.", ++ "nullable": true ++ }, ++ "EnableTranscoding": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to enable transcoding.", ++ "nullable": true ++ }, ++ "AllowVideoStreamCopy": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to enable video stream copy.", ++ "nullable": true ++ }, ++ "AllowAudioStreamCopy": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to allow audio stream copy.", ++ "nullable": true ++ }, ++ "AutoOpenLiveStream": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to auto open the live stream.", ++ "nullable": true ++ }, ++ "AlwaysBurnInSubtitleWhenTranscoding": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether always burn in subtitles when transcoding.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Playback info dto." ++ }, ++ "PlaybackInfoResponse": { ++ "type": "object", ++ "properties": { ++ "MediaSources": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaSourceInfo" ++ }, ++ "description": "Gets or sets the media sources." ++ }, ++ "PlaySessionId": { ++ "type": "string", ++ "description": "Gets or sets the play session identifier.", ++ "nullable": true ++ }, ++ "ErrorCode": { ++ "enum": [ ++ "NotAllowed", ++ "NoCompatibleStream", ++ "RateLimitExceeded" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackErrorCode" ++ } ++ ], ++ "description": "Gets or sets the error code.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class PlaybackInfoResponse." ++ }, ++ "PlaybackOrder": { ++ "enum": [ ++ "Default", ++ "Shuffle" ++ ], ++ "type": "string", ++ "description": "Enum PlaybackOrder." ++ }, ++ "PlaybackProgressInfo": { ++ "type": "object", ++ "properties": { ++ "CanSeek": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance can seek." ++ }, ++ "Item": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ ], ++ "description": "Gets or sets the item.", ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "description": "Gets or sets the item identifier.", ++ "format": "uuid" ++ }, ++ "SessionId": { ++ "type": "string", ++ "description": "Gets or sets the session id.", ++ "nullable": true ++ }, ++ "MediaSourceId": { ++ "type": "string", ++ "description": "Gets or sets the media version identifier.", ++ "nullable": true ++ }, ++ "AudioStreamIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the audio stream.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SubtitleStreamIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the subtitle stream.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IsPaused": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is paused." ++ }, ++ "IsMuted": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is muted." ++ }, ++ "PositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the position ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "PlaybackStartTimeTicks": { ++ "type": "integer", ++ "format": "int64", ++ "nullable": true ++ }, ++ "VolumeLevel": { ++ "type": "integer", ++ "description": "Gets or sets the volume level.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Brightness": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "AspectRatio": { ++ "type": "string", ++ "nullable": true ++ }, ++ "PlayMethod": { ++ "enum": [ ++ "Transcode", ++ "DirectStream", ++ "DirectPlay" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayMethod" ++ } ++ ], ++ "description": "Gets or sets the play method." ++ }, ++ "LiveStreamId": { ++ "type": "string", ++ "description": "Gets or sets the live stream identifier.", ++ "nullable": true ++ }, ++ "PlaySessionId": { ++ "type": "string", ++ "description": "Gets or sets the play session identifier.", ++ "nullable": true ++ }, ++ "RepeatMode": { ++ "enum": [ ++ "RepeatNone", ++ "RepeatAll", ++ "RepeatOne" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RepeatMode" ++ } ++ ], ++ "description": "Gets or sets the repeat mode." ++ }, ++ "PlaybackOrder": { ++ "enum": [ ++ "Default", ++ "Shuffle" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackOrder" ++ } ++ ], ++ "description": "Gets or sets the playback order." ++ }, ++ "NowPlayingQueue": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/QueueItem" ++ }, ++ "nullable": true ++ }, ++ "PlaylistItemId": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class PlaybackProgressInfo." ++ }, ++ "PlaybackRequestType": { ++ "enum": [ ++ "Play", ++ "SetPlaylistItem", ++ "RemoveFromPlaylist", ++ "MovePlaylistItem", ++ "Queue", ++ "Unpause", ++ "Pause", ++ "Stop", ++ "Seek", ++ "Buffer", ++ "Ready", ++ "NextItem", ++ "PreviousItem", ++ "SetRepeatMode", ++ "SetShuffleMode", ++ "Ping", ++ "IgnoreWait" ++ ], ++ "type": "string", ++ "description": "Enum PlaybackRequestType." ++ }, ++ "PlaybackStartInfo": { ++ "type": "object", ++ "properties": { ++ "CanSeek": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance can seek." ++ }, ++ "Item": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ ], ++ "description": "Gets or sets the item.", ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "description": "Gets or sets the item identifier.", ++ "format": "uuid" ++ }, ++ "SessionId": { ++ "type": "string", ++ "description": "Gets or sets the session id.", ++ "nullable": true ++ }, ++ "MediaSourceId": { ++ "type": "string", ++ "description": "Gets or sets the media version identifier.", ++ "nullable": true ++ }, ++ "AudioStreamIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the audio stream.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SubtitleStreamIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the subtitle stream.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IsPaused": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is paused." ++ }, ++ "IsMuted": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is muted." ++ }, ++ "PositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the position ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "PlaybackStartTimeTicks": { ++ "type": "integer", ++ "format": "int64", ++ "nullable": true ++ }, ++ "VolumeLevel": { ++ "type": "integer", ++ "description": "Gets or sets the volume level.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Brightness": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "AspectRatio": { ++ "type": "string", ++ "nullable": true ++ }, ++ "PlayMethod": { ++ "enum": [ ++ "Transcode", ++ "DirectStream", ++ "DirectPlay" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayMethod" ++ } ++ ], ++ "description": "Gets or sets the play method." ++ }, ++ "LiveStreamId": { ++ "type": "string", ++ "description": "Gets or sets the live stream identifier.", ++ "nullable": true ++ }, ++ "PlaySessionId": { ++ "type": "string", ++ "description": "Gets or sets the play session identifier.", ++ "nullable": true ++ }, ++ "RepeatMode": { ++ "enum": [ ++ "RepeatNone", ++ "RepeatAll", ++ "RepeatOne" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RepeatMode" ++ } ++ ], ++ "description": "Gets or sets the repeat mode." ++ }, ++ "PlaybackOrder": { ++ "enum": [ ++ "Default", ++ "Shuffle" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackOrder" ++ } ++ ], ++ "description": "Gets or sets the playback order." ++ }, ++ "NowPlayingQueue": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/QueueItem" ++ }, ++ "nullable": true ++ }, ++ "PlaylistItemId": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class PlaybackStartInfo." ++ }, ++ "PlaybackStopInfo": { ++ "type": "object", ++ "properties": { ++ "Item": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ ], ++ "description": "Gets or sets the item.", ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "description": "Gets or sets the item identifier.", ++ "format": "uuid" ++ }, ++ "SessionId": { ++ "type": "string", ++ "description": "Gets or sets the session id.", ++ "nullable": true ++ }, ++ "MediaSourceId": { ++ "type": "string", ++ "description": "Gets or sets the media version identifier.", ++ "nullable": true ++ }, ++ "PositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the position ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "LiveStreamId": { ++ "type": "string", ++ "description": "Gets or sets the live stream identifier.", ++ "nullable": true ++ }, ++ "PlaySessionId": { ++ "type": "string", ++ "description": "Gets or sets the play session identifier.", ++ "nullable": true ++ }, ++ "Failed": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Session.PlaybackStopInfo is failed." ++ }, ++ "NextMediaType": { ++ "type": "string", ++ "nullable": true ++ }, ++ "PlaylistItemId": { ++ "type": "string", ++ "nullable": true ++ }, ++ "NowPlayingQueue": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/QueueItem" ++ }, ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class PlaybackStopInfo." ++ }, ++ "PlayCommand": { ++ "enum": [ ++ "PlayNow", ++ "PlayNext", ++ "PlayLast", ++ "PlayInstantMix", ++ "PlayShuffle" ++ ], ++ "type": "string", ++ "description": "Enum PlayCommand." ++ }, ++ "PlayerStateInfo": { ++ "type": "object", ++ "properties": { ++ "PositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the now playing position ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "CanSeek": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance can seek." ++ }, ++ "IsPaused": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is paused." ++ }, ++ "IsMuted": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is muted." ++ }, ++ "VolumeLevel": { ++ "type": "integer", ++ "description": "Gets or sets the volume level.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "AudioStreamIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the now playing audio stream.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "SubtitleStreamIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the now playing subtitle stream.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MediaSourceId": { ++ "type": "string", ++ "description": "Gets or sets the now playing media version identifier.", ++ "nullable": true ++ }, ++ "PlayMethod": { ++ "enum": [ ++ "Transcode", ++ "DirectStream", ++ "DirectPlay" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayMethod" ++ } ++ ], ++ "description": "Gets or sets the play method.", ++ "nullable": true ++ }, ++ "RepeatMode": { ++ "enum": [ ++ "RepeatNone", ++ "RepeatAll", ++ "RepeatOne" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RepeatMode" ++ } ++ ], ++ "description": "Gets or sets the repeat mode." ++ }, ++ "PlaybackOrder": { ++ "enum": [ ++ "Default", ++ "Shuffle" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaybackOrder" ++ } ++ ], ++ "description": "Gets or sets the playback order." ++ }, ++ "LiveStreamId": { ++ "type": "string", ++ "description": "Gets or sets the now playing live stream identifier.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "PlaylistCreationResult": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "PlaylistDto": { ++ "type": "object", ++ "properties": { ++ "OpenAccess": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the playlist is publicly readable." ++ }, ++ "Shares": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PlaylistUserPermissions" ++ }, ++ "description": "Gets or sets the share permissions." ++ }, ++ "ItemIds": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "description": "Gets or sets the item ids." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "DTO for playlists." ++ }, ++ "PlaylistUserPermissions": { ++ "type": "object", ++ "properties": { ++ "UserId": { ++ "type": "string", ++ "description": "Gets or sets the user id.", ++ "format": "uuid" ++ }, ++ "CanEdit": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the user has edit permissions." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class to hold data on user permissions for playlists." ++ }, ++ "PlayMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayRequest" ++ } ++ ], ++ "description": "Class PlayRequest.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "Play", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Play command websocket message." ++ }, ++ "PlayMethod": { ++ "enum": [ ++ "Transcode", ++ "DirectStream", ++ "DirectPlay" ++ ], ++ "type": "string" ++ }, ++ "PlayQueueUpdate": { ++ "type": "object", ++ "properties": { ++ "Reason": { ++ "enum": [ ++ "NewPlaylist", ++ "SetCurrentItem", ++ "RemoveItems", ++ "MoveItem", ++ "Queue", ++ "QueueNext", ++ "NextItem", ++ "PreviousItem", ++ "RepeatMode", ++ "ShuffleMode" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayQueueUpdateReason" ++ } ++ ], ++ "description": "Gets the request type that originated this update." ++ }, ++ "LastUpdate": { ++ "type": "string", ++ "description": "Gets the UTC time of the last change to the playing queue.", ++ "format": "date-time" ++ }, ++ "Playlist": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SyncPlayQueueItem" ++ }, ++ "description": "Gets the playlist." ++ }, ++ "PlayingItemIndex": { ++ "type": "integer", ++ "description": "Gets the playing item index in the playlist.", ++ "format": "int32" ++ }, ++ "StartPositionTicks": { ++ "type": "integer", ++ "description": "Gets the start position ticks.", ++ "format": "int64" ++ }, ++ "IsPlaying": { ++ "type": "boolean", ++ "description": "Gets a value indicating whether the current item is playing." ++ }, ++ "ShuffleMode": { ++ "enum": [ ++ "Sorted", ++ "Shuffle" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupShuffleMode" ++ } ++ ], ++ "description": "Gets the shuffle mode." ++ }, ++ "RepeatMode": { ++ "enum": [ ++ "RepeatOne", ++ "RepeatAll", ++ "RepeatNone" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupRepeatMode" ++ } ++ ], ++ "description": "Gets the repeat mode." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class PlayQueueUpdate." ++ }, ++ "PlayQueueUpdateReason": { ++ "enum": [ ++ "NewPlaylist", ++ "SetCurrentItem", ++ "RemoveItems", ++ "MoveItem", ++ "Queue", ++ "QueueNext", ++ "NextItem", ++ "PreviousItem", ++ "RepeatMode", ++ "ShuffleMode" ++ ], ++ "type": "string", ++ "description": "Enum PlayQueueUpdateReason." ++ }, ++ "PlayRequest": { ++ "type": "object", ++ "properties": { ++ "ItemIds": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "description": "Gets or sets the item ids.", ++ "nullable": true ++ }, ++ "StartPositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the start position ticks that the first item should be played at.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "PlayCommand": { ++ "enum": [ ++ "PlayNow", ++ "PlayNext", ++ "PlayLast", ++ "PlayInstantMix", ++ "PlayShuffle" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayCommand" ++ } ++ ], ++ "description": "Gets or sets the play command." ++ }, ++ "ControllingUserId": { ++ "type": "string", ++ "description": "Gets or sets the controlling user identifier.", ++ "format": "uuid" ++ }, ++ "SubtitleStreamIndex": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "AudioStreamIndex": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MediaSourceId": { ++ "type": "string", ++ "nullable": true ++ }, ++ "StartIndex": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class PlayRequest." ++ }, ++ "PlayRequestDto": { ++ "type": "object", ++ "properties": { ++ "PlayingQueue": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "description": "Gets or sets the playing queue." ++ }, ++ "PlayingItemPosition": { ++ "type": "integer", ++ "description": "Gets or sets the position of the playing item in the queue.", ++ "format": "int32" ++ }, ++ "StartPositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the start position ticks.", ++ "format": "int64" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class PlayRequestDto." ++ }, ++ "PlaystateCommand": { ++ "enum": [ ++ "Stop", ++ "Pause", ++ "Unpause", ++ "NextTrack", ++ "PreviousTrack", ++ "Seek", ++ "Rewind", ++ "FastForward", ++ "PlayPause" ++ ], ++ "type": "string", ++ "description": "Enum PlaystateCommand." ++ }, ++ "PlaystateMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaystateRequest" ++ } ++ ], ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "Playstate", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Playstate message." ++ }, ++ "PlaystateRequest": { ++ "type": "object", ++ "properties": { ++ "Command": { ++ "enum": [ ++ "Stop", ++ "Pause", ++ "Unpause", ++ "NextTrack", ++ "PreviousTrack", ++ "Seek", ++ "Rewind", ++ "FastForward", ++ "PlayPause" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlaystateCommand" ++ } ++ ], ++ "description": "Enum PlaystateCommand." ++ }, ++ "SeekPositionTicks": { ++ "type": "integer", ++ "format": "int64", ++ "nullable": true ++ }, ++ "ControllingUserId": { ++ "type": "string", ++ "description": "Gets or sets the controlling user identifier.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "PluginInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name." ++ }, ++ "Version": { ++ "type": "string", ++ "description": "Gets or sets the version." ++ }, ++ "ConfigurationFileName": { ++ "type": "string", ++ "description": "Gets or sets the name of the configuration file.", ++ "nullable": true ++ }, ++ "Description": { ++ "type": "string", ++ "description": "Gets or sets the description." ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the unique id.", ++ "format": "uuid" ++ }, ++ "CanUninstall": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the plugin can be uninstalled." ++ }, ++ "HasImage": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this plugin has a valid image." ++ }, ++ "Status": { ++ "enum": [ ++ "Active", ++ "Restart", ++ "Deleted", ++ "Superseded", ++ "Superceded", ++ "Malfunctioned", ++ "NotSupported", ++ "Disabled" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PluginStatus" ++ } ++ ], ++ "description": "Gets or sets a value indicating the status of the plugin." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "This is a serializable stub class that is used by the api to provide information about installed plugins." ++ }, ++ "PluginInstallationCancelledMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/InstallationInfo" ++ } ++ ], ++ "description": "Class InstallationInfo.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "PackageInstallationCancelled", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Plugin installation cancelled message." ++ }, ++ "PluginInstallationCompletedMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/InstallationInfo" ++ } ++ ], ++ "description": "Class InstallationInfo.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "PackageInstallationCompleted", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Plugin installation completed message." ++ }, ++ "PluginInstallationFailedMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/InstallationInfo" ++ } ++ ], ++ "description": "Class InstallationInfo.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "PackageInstallationFailed", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Plugin installation failed message." ++ }, ++ "PluginInstallingMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/InstallationInfo" ++ } ++ ], ++ "description": "Class InstallationInfo.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "PackageInstalling", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Package installing message." ++ }, ++ "PluginStatus": { ++ "enum": [ ++ "Active", ++ "Restart", ++ "Deleted", ++ "Superseded", ++ "Superceded", ++ "Malfunctioned", ++ "NotSupported", ++ "Disabled" ++ ], ++ "type": "string", ++ "description": "Plugin load status." ++ }, ++ "PluginUninstalledMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PluginInfo" ++ } ++ ], ++ "description": "This is a serializable stub class that is used by the api to provide information about installed plugins.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "PackageUninstalled", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Plugin uninstalled message." ++ }, ++ "PreviousItemRequestDto": { ++ "type": "object", ++ "properties": { ++ "PlaylistItemId": { ++ "type": "string", ++ "description": "Gets or sets the playing item identifier.", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class PreviousItemRequestDto." ++ }, ++ "ProblemDetails": { ++ "type": "object", ++ "properties": { ++ "type": { ++ "type": "string", ++ "nullable": true ++ }, ++ "title": { ++ "type": "string", ++ "nullable": true ++ }, ++ "status": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "detail": { ++ "type": "string", ++ "nullable": true ++ }, ++ "instance": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": { } ++ }, ++ "ProcessPriorityClass": { ++ "enum": [ ++ "Normal", ++ "Idle", ++ "High", ++ "RealTime", ++ "BelowNormal", ++ "AboveNormal" ++ ], ++ "type": "string" ++ }, ++ "ProfileCondition": { ++ "type": "object", ++ "properties": { ++ "Condition": { ++ "enum": [ ++ "Equals", ++ "NotEquals", ++ "LessThanEqual", ++ "GreaterThanEqual", ++ "EqualsAny" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ProfileConditionType" ++ } ++ ] ++ }, ++ "Property": { ++ "enum": [ ++ "AudioChannels", ++ "AudioBitrate", ++ "AudioProfile", ++ "Width", ++ "Height", ++ "Has64BitOffsets", ++ "PacketLength", ++ "VideoBitDepth", ++ "VideoBitrate", ++ "VideoFramerate", ++ "VideoLevel", ++ "VideoProfile", ++ "VideoTimestamp", ++ "IsAnamorphic", ++ "RefFrames", ++ "NumAudioStreams", ++ "NumVideoStreams", ++ "IsSecondaryAudio", ++ "VideoCodecTag", ++ "IsAvc", ++ "IsInterlaced", ++ "AudioSampleRate", ++ "AudioBitDepth", ++ "VideoRangeType", ++ "NumStreams" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ProfileConditionValue" ++ } ++ ] ++ }, ++ "Value": { ++ "type": "string", ++ "nullable": true ++ }, ++ "IsRequired": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "ProfileConditionType": { ++ "enum": [ ++ "Equals", ++ "NotEquals", ++ "LessThanEqual", ++ "GreaterThanEqual", ++ "EqualsAny" ++ ], ++ "type": "string" ++ }, ++ "ProfileConditionValue": { ++ "enum": [ ++ "AudioChannels", ++ "AudioBitrate", ++ "AudioProfile", ++ "Width", ++ "Height", ++ "Has64BitOffsets", ++ "PacketLength", ++ "VideoBitDepth", ++ "VideoBitrate", ++ "VideoFramerate", ++ "VideoLevel", ++ "VideoProfile", ++ "VideoTimestamp", ++ "IsAnamorphic", ++ "RefFrames", ++ "NumAudioStreams", ++ "NumVideoStreams", ++ "IsSecondaryAudio", ++ "VideoCodecTag", ++ "IsAvc", ++ "IsInterlaced", ++ "AudioSampleRate", ++ "AudioBitDepth", ++ "VideoRangeType", ++ "NumStreams" ++ ], ++ "type": "string" ++ }, ++ "ProgramAudio": { ++ "enum": [ ++ "Mono", ++ "Stereo", ++ "Dolby", ++ "DolbyDigital", ++ "Thx", ++ "Atmos" ++ ], ++ "type": "string" ++ }, ++ "PublicSystemInfo": { ++ "type": "object", ++ "properties": { ++ "LocalAddress": { ++ "type": "string", ++ "description": "Gets or sets the local address.", ++ "nullable": true ++ }, ++ "ServerName": { ++ "type": "string", ++ "description": "Gets or sets the name of the server.", ++ "nullable": true ++ }, ++ "Version": { ++ "type": "string", ++ "description": "Gets or sets the server version.", ++ "nullable": true ++ }, ++ "ProductName": { ++ "type": "string", ++ "description": "Gets or sets the product name. This is the AssemblyProduct name.", ++ "nullable": true ++ }, ++ "OperatingSystem": { ++ "type": "string", ++ "description": "Gets or sets the operating system.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the id.", ++ "nullable": true ++ }, ++ "StartupWizardCompleted": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the startup wizard is completed.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "QueryFilters": { ++ "type": "object", ++ "properties": { ++ "Genres": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameGuidPair" ++ }, ++ "nullable": true ++ }, ++ "Tags": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "QueryFiltersLegacy": { ++ "type": "object", ++ "properties": { ++ "Genres": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "Tags": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "OfficialRatings": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "Years": { ++ "type": "array", ++ "items": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "QueueItem": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "PlaylistItemId": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "QueueRequestDto": { ++ "type": "object", ++ "properties": { ++ "ItemIds": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "description": "Gets or sets the items to enqueue." ++ }, ++ "Mode": { ++ "enum": [ ++ "Queue", ++ "QueueNext" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupQueueMode" ++ } ++ ], ++ "description": "Enum GroupQueueMode." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class QueueRequestDto." ++ }, ++ "QuickConnectDto": { ++ "required": [ ++ "Secret" ++ ], ++ "type": "object", ++ "properties": { ++ "Secret": { ++ "type": "string", ++ "description": "Gets or sets the quick connect secret." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The quick connect request body." ++ }, ++ "QuickConnectResult": { ++ "type": "object", ++ "properties": { ++ "Authenticated": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this request is authorized." ++ }, ++ "Secret": { ++ "type": "string", ++ "description": "Gets the secret value used to uniquely identify this request. Can be used to retrieve authentication information." ++ }, ++ "Code": { ++ "type": "string", ++ "description": "Gets the user facing code used so the user can quickly differentiate this request from others." ++ }, ++ "DeviceId": { ++ "type": "string", ++ "description": "Gets the requesting device id." ++ }, ++ "DeviceName": { ++ "type": "string", ++ "description": "Gets the requesting device name." ++ }, ++ "AppName": { ++ "type": "string", ++ "description": "Gets the requesting app name." ++ }, ++ "AppVersion": { ++ "type": "string", ++ "description": "Gets the requesting app version." ++ }, ++ "DateAdded": { ++ "type": "string", ++ "description": "Gets or sets the DateTime that this request was created.", ++ "format": "date-time" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Stores the state of an quick connect request." ++ }, ++ "RatingType": { ++ "enum": [ ++ "Score", ++ "Likes" ++ ], ++ "type": "string" ++ }, ++ "ReadyRequestDto": { ++ "type": "object", ++ "properties": { ++ "When": { ++ "type": "string", ++ "description": "Gets or sets when the request has been made by the client.", ++ "format": "date-time" ++ }, ++ "PositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the position ticks.", ++ "format": "int64" ++ }, ++ "IsPlaying": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the client playback is unpaused." ++ }, ++ "PlaylistItemId": { ++ "type": "string", ++ "description": "Gets or sets the playlist item identifier of the playing item.", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class ReadyRequest." ++ }, ++ "RecommendationDto": { ++ "type": "object", ++ "properties": { ++ "Items": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ }, ++ "nullable": true ++ }, ++ "RecommendationType": { ++ "enum": [ ++ "SimilarToRecentlyPlayed", ++ "SimilarToLikedItem", ++ "HasDirectorFromRecentlyPlayed", ++ "HasActorFromRecentlyPlayed", ++ "HasLikedDirector", ++ "HasLikedActor" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RecommendationType" ++ } ++ ] ++ }, ++ "BaselineItemName": { ++ "type": "string", ++ "nullable": true ++ }, ++ "CategoryId": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "RecommendationType": { ++ "enum": [ ++ "SimilarToRecentlyPlayed", ++ "SimilarToLikedItem", ++ "HasDirectorFromRecentlyPlayed", ++ "HasActorFromRecentlyPlayed", ++ "HasLikedDirector", ++ "HasLikedActor" ++ ], ++ "type": "string" ++ }, ++ "RecordingStatus": { ++ "enum": [ ++ "New", ++ "InProgress", ++ "Completed", ++ "Cancelled", ++ "ConflictedOk", ++ "ConflictedNotOk", ++ "Error" ++ ], ++ "type": "string" ++ }, ++ "RefreshProgressMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "RefreshProgress", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Refresh progress message." ++ }, ++ "RemoteImageInfo": { ++ "type": "object", ++ "properties": { ++ "ProviderName": { ++ "type": "string", ++ "description": "Gets or sets the name of the provider.", ++ "nullable": true ++ }, ++ "Url": { ++ "type": "string", ++ "description": "Gets or sets the URL.", ++ "nullable": true ++ }, ++ "ThumbnailUrl": { ++ "type": "string", ++ "description": "Gets or sets a url used for previewing a smaller version.", ++ "nullable": true ++ }, ++ "Height": { ++ "type": "integer", ++ "description": "Gets or sets the height.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Width": { ++ "type": "integer", ++ "description": "Gets or sets the width.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "CommunityRating": { ++ "type": "number", ++ "description": "Gets or sets the community rating.", ++ "format": "double", ++ "nullable": true ++ }, ++ "VoteCount": { ++ "type": "integer", ++ "description": "Gets or sets the vote count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Language": { ++ "type": "string", ++ "description": "Gets or sets the language.", ++ "nullable": true ++ }, ++ "Type": { ++ "enum": [ ++ "Primary", ++ "Art", ++ "Backdrop", ++ "Banner", ++ "Logo", ++ "Thumb", ++ "Disc", ++ "Box", ++ "Screenshot", ++ "Menu", ++ "Chapter", ++ "BoxRear", ++ "Profile" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageType" ++ } ++ ], ++ "description": "Gets or sets the type." ++ }, ++ "RatingType": { ++ "enum": [ ++ "Score", ++ "Likes" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RatingType" ++ } ++ ], ++ "description": "Gets or sets the type of the rating." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class RemoteImageInfo." ++ }, ++ "RemoteImageResult": { ++ "type": "object", ++ "properties": { ++ "Images": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteImageInfo" ++ }, ++ "description": "Gets or sets the images.", ++ "nullable": true ++ }, ++ "TotalRecordCount": { ++ "type": "integer", ++ "description": "Gets or sets the total record count.", ++ "format": "int32" ++ }, ++ "Providers": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the providers.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class RemoteImageResult." ++ }, ++ "RemoteLyricInfoDto": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the id for the lyric." ++ }, ++ "ProviderName": { ++ "type": "string", ++ "description": "Gets the provider name." ++ }, ++ "Lyrics": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/LyricDto" ++ } ++ ], ++ "description": "Gets the lyrics." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The remote lyric info dto." ++ }, ++ "RemoteSearchResult": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "ProductionYear": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumberEnd": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "ImageUrl": { ++ "type": "string", ++ "nullable": true ++ }, ++ "SearchProviderName": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Overview": { ++ "type": "string", ++ "nullable": true ++ }, ++ "AlbumArtist": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ } ++ ], ++ "nullable": true ++ }, ++ "Artists": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RemoteSearchResult" ++ }, ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "RemoteSubtitleInfo": { ++ "type": "object", ++ "properties": { ++ "ThreeLetterISOLanguageName": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ProviderName": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Name": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Format": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Author": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Comment": { ++ "type": "string", ++ "nullable": true ++ }, ++ "DateCreated": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "CommunityRating": { ++ "type": "number", ++ "format": "float", ++ "nullable": true ++ }, ++ "FrameRate": { ++ "type": "number", ++ "format": "float", ++ "nullable": true ++ }, ++ "DownloadCount": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IsHashMatch": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "AiTranslated": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "MachineTranslated": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "Forced": { ++ "type": "boolean", ++ "nullable": true ++ }, ++ "HearingImpaired": { ++ "type": "boolean", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "RemoveFromPlaylistRequestDto": { ++ "type": "object", ++ "properties": { ++ "PlaylistItemIds": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "description": "Gets or sets the playlist identifiers of the items. Ignored when clearing the playlist." ++ }, ++ "ClearPlaylist": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the entire playlist should be cleared." ++ }, ++ "ClearPlayingItem": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the playing item should be removed as well. Used only when clearing the playlist." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class RemoveFromPlaylistRequestDto." ++ }, ++ "RepeatMode": { ++ "enum": [ ++ "RepeatNone", ++ "RepeatAll", ++ "RepeatOne" ++ ], ++ "type": "string" ++ }, ++ "RepositoryInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "Url": { ++ "type": "string", ++ "description": "Gets or sets the URL.", ++ "nullable": true ++ }, ++ "Enabled": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the repository is enabled." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class RepositoryInfo." ++ }, ++ "RestartRequiredMessage": { ++ "type": "object", ++ "properties": { ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "RestartRequired", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Restart required." ++ }, ++ "ScheduledTaskEndedMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TaskResult" ++ } ++ ], ++ "description": "Class TaskExecutionInfo.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "ScheduledTaskEnded", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Scheduled task ended message." ++ }, ++ "ScheduledTasksInfoMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TaskInfo" ++ }, ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "ScheduledTasksInfo", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Scheduled tasks info message." ++ }, ++ "ScheduledTasksInfoStartMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "type": "string", ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "ScheduledTasksInfoStart", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Scheduled tasks info start message.\r\nData is the timing data encoded as \"$initialDelay,$interval\" in ms." ++ }, ++ "ScheduledTasksInfoStopMessage": { ++ "type": "object", ++ "properties": { ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "ScheduledTasksInfoStop", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Scheduled tasks info stop message." ++ }, ++ "ScrollDirection": { ++ "enum": [ ++ "Horizontal", ++ "Vertical" ++ ], ++ "type": "string", ++ "description": "An enum representing the axis that should be scrolled." ++ }, ++ "SearchHint": { ++ "type": "object", ++ "properties": { ++ "ItemId": { ++ "type": "string", ++ "description": "Gets or sets the item id.", ++ "format": "uuid", ++ "deprecated": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the item id.", ++ "format": "uuid" ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name." ++ }, ++ "MatchedTerm": { ++ "type": "string", ++ "description": "Gets or sets the matched term.", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "description": "Gets or sets the index number.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ProductionYear": { ++ "type": "integer", ++ "description": "Gets or sets the production year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "description": "Gets or sets the parent index number.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PrimaryImageTag": { ++ "type": "string", ++ "description": "Gets or sets the image tag.", ++ "nullable": true ++ }, ++ "ThumbImageTag": { ++ "type": "string", ++ "description": "Gets or sets the thumb image tag.", ++ "nullable": true ++ }, ++ "ThumbImageItemId": { ++ "type": "string", ++ "description": "Gets or sets the thumb image item identifier.", ++ "nullable": true ++ }, ++ "BackdropImageTag": { ++ "type": "string", ++ "description": "Gets or sets the backdrop image tag.", ++ "nullable": true ++ }, ++ "BackdropImageItemId": { ++ "type": "string", ++ "description": "Gets or sets the backdrop image item identifier.", ++ "nullable": true ++ }, ++ "Type": { ++ "enum": [ ++ "AggregateFolder", ++ "Audio", ++ "AudioBook", ++ "BasePluginFolder", ++ "Book", ++ "BoxSet", ++ "Channel", ++ "ChannelFolderItem", ++ "CollectionFolder", ++ "Episode", ++ "Folder", ++ "Genre", ++ "ManualPlaylistsFolder", ++ "Movie", ++ "LiveTvChannel", ++ "LiveTvProgram", ++ "MusicAlbum", ++ "MusicArtist", ++ "MusicGenre", ++ "MusicVideo", ++ "Person", ++ "Photo", ++ "PhotoAlbum", ++ "Playlist", ++ "PlaylistsFolder", ++ "Program", ++ "Recording", ++ "Season", ++ "Series", ++ "Studio", ++ "Trailer", ++ "TvChannel", ++ "TvProgram", ++ "UserRootFolder", ++ "UserView", ++ "Video", ++ "Year" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemKind" ++ } ++ ], ++ "description": "Gets or sets the type." ++ }, ++ "IsFolder": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is folder.", ++ "nullable": true ++ }, ++ "RunTimeTicks": { ++ "type": "integer", ++ "description": "Gets or sets the run time ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "MediaType": { ++ "enum": [ ++ "Unknown", ++ "Video", ++ "Audio", ++ "Photo", ++ "Book" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaType" ++ } ++ ], ++ "description": "Gets or sets the type of the media.", ++ "default": "Unknown" ++ }, ++ "StartDate": { ++ "type": "string", ++ "description": "Gets or sets the start date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "EndDate": { ++ "type": "string", ++ "description": "Gets or sets the end date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "Series": { ++ "type": "string", ++ "description": "Gets or sets the series.", ++ "nullable": true ++ }, ++ "Status": { ++ "type": "string", ++ "description": "Gets or sets the status.", ++ "nullable": true ++ }, ++ "Album": { ++ "type": "string", ++ "description": "Gets or sets the album.", ++ "nullable": true ++ }, ++ "AlbumId": { ++ "type": "string", ++ "description": "Gets or sets the album id.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "AlbumArtist": { ++ "type": "string", ++ "description": "Gets or sets the album artist.", ++ "nullable": true ++ }, ++ "Artists": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the artists." ++ }, ++ "SongCount": { ++ "type": "integer", ++ "description": "Gets or sets the song count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "EpisodeCount": { ++ "type": "integer", ++ "description": "Gets or sets the episode count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ChannelId": { ++ "type": "string", ++ "description": "Gets or sets the channel identifier.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "ChannelName": { ++ "type": "string", ++ "description": "Gets or sets the name of the channel.", ++ "nullable": true ++ }, ++ "PrimaryImageAspectRatio": { ++ "type": "number", ++ "description": "Gets or sets the primary image aspect ratio.", ++ "format": "double", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class SearchHintResult." ++ }, ++ "SearchHintResult": { ++ "type": "object", ++ "properties": { ++ "SearchHints": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SearchHint" ++ }, ++ "description": "Gets the search hints." ++ }, ++ "TotalRecordCount": { ++ "type": "integer", ++ "description": "Gets the total record count.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class SearchHintResult." ++ }, ++ "SeekRequestDto": { ++ "type": "object", ++ "properties": { ++ "PositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the position ticks.", ++ "format": "int64" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class SeekRequestDto." ++ }, ++ "SendCommand": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid" ++ }, ++ "PlaylistItemId": { ++ "type": "string", ++ "description": "Gets the playlist identifier of the playing item.", ++ "format": "uuid" ++ }, ++ "When": { ++ "type": "string", ++ "description": "Gets or sets the UTC time when to execute the command.", ++ "format": "date-time" ++ }, ++ "PositionTicks": { ++ "type": "integer", ++ "description": "Gets the position ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "Command": { ++ "enum": [ ++ "Unpause", ++ "Pause", ++ "Stop", ++ "Seek" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SendCommandType" ++ } ++ ], ++ "description": "Gets the command." ++ }, ++ "EmittedAt": { ++ "type": "string", ++ "description": "Gets the UTC time when this command has been emitted.", ++ "format": "date-time" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class SendCommand." ++ }, ++ "SendCommandType": { ++ "enum": [ ++ "Unpause", ++ "Pause", ++ "Stop", ++ "Seek" ++ ], ++ "type": "string", ++ "description": "Enum SendCommandType." ++ }, ++ "SeriesInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "description": "Gets or sets the original title.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "MetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "Year": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsAutomated": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SeriesInfoRemoteSearchQuery": { ++ "type": "object", ++ "properties": { ++ "SearchInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SeriesInfo" ++ } ++ ], ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "SearchProviderName": { ++ "type": "string", ++ "description": "Gets or sets the provider name to search within if set.", ++ "nullable": true ++ }, ++ "IncludeDisabledProviders": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether disabled providers should be included." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SeriesStatus": { ++ "enum": [ ++ "Continuing", ++ "Ended", ++ "Unreleased" ++ ], ++ "type": "string", ++ "description": "The status of a series." ++ }, ++ "SeriesTimerCancelledMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TimerEventInfo" ++ } ++ ], ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "SeriesTimerCancelled", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Series timer cancelled message." ++ }, ++ "SeriesTimerCreatedMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TimerEventInfo" ++ } ++ ], ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "SeriesTimerCreated", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Series timer created message." ++ }, ++ "SeriesTimerInfoDto": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the Id of the recording.", ++ "nullable": true ++ }, ++ "Type": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ServerId": { ++ "type": "string", ++ "description": "Gets or sets the server identifier.", ++ "nullable": true ++ }, ++ "ExternalId": { ++ "type": "string", ++ "description": "Gets or sets the external identifier.", ++ "nullable": true ++ }, ++ "ChannelId": { ++ "type": "string", ++ "description": "Gets or sets the channel id of the recording.", ++ "format": "uuid" ++ }, ++ "ExternalChannelId": { ++ "type": "string", ++ "description": "Gets or sets the external channel identifier.", ++ "nullable": true ++ }, ++ "ChannelName": { ++ "type": "string", ++ "description": "Gets or sets the channel name of the recording.", ++ "nullable": true ++ }, ++ "ChannelPrimaryImageTag": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ProgramId": { ++ "type": "string", ++ "description": "Gets or sets the program identifier.", ++ "nullable": true ++ }, ++ "ExternalProgramId": { ++ "type": "string", ++ "description": "Gets or sets the external program identifier.", ++ "nullable": true ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name of the recording.", ++ "nullable": true ++ }, ++ "Overview": { ++ "type": "string", ++ "description": "Gets or sets the description of the recording.", ++ "nullable": true ++ }, ++ "StartDate": { ++ "type": "string", ++ "description": "Gets or sets the start date of the recording, in UTC.", ++ "format": "date-time" ++ }, ++ "EndDate": { ++ "type": "string", ++ "description": "Gets or sets the end date of the recording, in UTC.", ++ "format": "date-time" ++ }, ++ "ServiceName": { ++ "type": "string", ++ "description": "Gets or sets the name of the service.", ++ "nullable": true ++ }, ++ "Priority": { ++ "type": "integer", ++ "description": "Gets or sets the priority.", ++ "format": "int32" ++ }, ++ "PrePaddingSeconds": { ++ "type": "integer", ++ "description": "Gets or sets the pre padding seconds.", ++ "format": "int32" ++ }, ++ "PostPaddingSeconds": { ++ "type": "integer", ++ "description": "Gets or sets the post padding seconds.", ++ "format": "int32" ++ }, ++ "IsPrePaddingRequired": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is pre padding required." ++ }, ++ "ParentBackdropItemId": { ++ "type": "string", ++ "description": "Gets or sets the Id of the Parent that has a backdrop if the item does not have one.", ++ "nullable": true ++ }, ++ "ParentBackdropImageTags": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the parent backdrop image tags.", ++ "nullable": true ++ }, ++ "IsPostPaddingRequired": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is post padding required." ++ }, ++ "KeepUntil": { ++ "enum": [ ++ "UntilDeleted", ++ "UntilSpaceNeeded", ++ "UntilWatched", ++ "UntilDate" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/KeepUntil" ++ } ++ ] ++ }, ++ "RecordAnyTime": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [record any time]." ++ }, ++ "SkipEpisodesInLibrary": { ++ "type": "boolean" ++ }, ++ "RecordAnyChannel": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [record any channel]." ++ }, ++ "KeepUpTo": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "RecordNewOnly": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [record new only]." ++ }, ++ "Days": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/DayOfWeek" ++ }, ++ "description": "Gets or sets the days.", ++ "nullable": true ++ }, ++ "DayPattern": { ++ "enum": [ ++ "Daily", ++ "Weekdays", ++ "Weekends" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DayPattern" ++ } ++ ], ++ "description": "Gets or sets the day pattern.", ++ "nullable": true ++ }, ++ "ImageTags": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the image tags.", ++ "nullable": true ++ }, ++ "ParentThumbItemId": { ++ "type": "string", ++ "description": "Gets or sets the parent thumb item id.", ++ "nullable": true ++ }, ++ "ParentThumbImageTag": { ++ "type": "string", ++ "description": "Gets or sets the parent thumb image tag.", ++ "nullable": true ++ }, ++ "ParentPrimaryImageItemId": { ++ "type": "string", ++ "description": "Gets or sets the parent primary image item identifier.", ++ "format": "uuid", ++ "nullable": true ++ }, ++ "ParentPrimaryImageTag": { ++ "type": "string", ++ "description": "Gets or sets the parent primary image tag.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class SeriesTimerInfoDto." ++ }, ++ "SeriesTimerInfoDtoQueryResult": { ++ "type": "object", ++ "properties": { ++ "Items": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SeriesTimerInfoDto" ++ }, ++ "description": "Gets or sets the items." ++ }, ++ "TotalRecordCount": { ++ "type": "integer", ++ "description": "Gets or sets the total number of records available.", ++ "format": "int32" ++ }, ++ "StartIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the first record in Items.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Query result container." ++ }, ++ "ServerConfiguration": { ++ "type": "object", ++ "properties": { ++ "LogFileRetentionDays": { ++ "type": "integer", ++ "description": "Gets or sets the number of days we should retain log files.", ++ "format": "int32" ++ }, ++ "IsStartupWizardCompleted": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is first run." ++ }, ++ "CachePath": { ++ "type": "string", ++ "description": "Gets or sets the cache path.", ++ "nullable": true ++ }, ++ "PreviousVersion": { ++ "type": "string", ++ "description": "Gets or sets the last known version that was ran using the configuration.", ++ "nullable": true ++ }, ++ "PreviousVersionStr": { ++ "type": "string", ++ "description": "Gets or sets the stringified PreviousVersion to be stored/loaded,\r\nbecause System.Version itself isn't xml-serializable.", ++ "nullable": true ++ }, ++ "EnableMetrics": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to enable prometheus metrics exporting." ++ }, ++ "EnableNormalizedItemByNameIds": { ++ "type": "boolean" ++ }, ++ "IsPortAuthorized": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is port authorized." ++ }, ++ "QuickConnectAvailable": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether quick connect is available for use on this server." ++ }, ++ "EnableCaseSensitiveItemIds": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [enable case-sensitive item ids]." ++ }, ++ "DisableLiveTvChannelUserDataName": { ++ "type": "boolean" ++ }, ++ "MetadataPath": { ++ "type": "string", ++ "description": "Gets or sets the metadata path." ++ }, ++ "PreferredMetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the preferred metadata language." ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code." ++ }, ++ "SortReplaceCharacters": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets characters to be replaced with a ' ' in strings to create a sort name." ++ }, ++ "SortRemoveCharacters": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets characters to be removed from strings to create a sort name." ++ }, ++ "SortRemoveWords": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets words to be removed from strings to create a sort name." ++ }, ++ "MinResumePct": { ++ "type": "integer", ++ "description": "Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.", ++ "format": "int32" ++ }, ++ "MaxResumePct": { ++ "type": "integer", ++ "description": "Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.", ++ "format": "int32" ++ }, ++ "MinResumeDurationSeconds": { ++ "type": "integer", ++ "description": "Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..", ++ "format": "int32" ++ }, ++ "MinAudiobookResume": { ++ "type": "integer", ++ "description": "Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated.", ++ "format": "int32" ++ }, ++ "MaxAudiobookResume": { ++ "type": "integer", ++ "description": "Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.", ++ "format": "int32" ++ }, ++ "InactiveSessionThreshold": { ++ "type": "integer", ++ "description": "Gets or sets the threshold in minutes after a inactive session gets closed automatically.\r\nIf set to 0 the check for inactive sessions gets disabled.", ++ "format": "int32" ++ }, ++ "LibraryMonitorDelay": { ++ "type": "integer", ++ "description": "Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed\r\nSome delay is necessary with some items because their creation is not atomic. It involves the creation of several\r\ndifferent directories and files.", ++ "format": "int32" ++ }, ++ "LibraryUpdateDuration": { ++ "type": "integer", ++ "description": "Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification.", ++ "format": "int32" ++ }, ++ "CacheSize": { ++ "type": "integer", ++ "description": "Gets or sets the maximum amount of items to cache.", ++ "format": "int32" ++ }, ++ "ImageSavingConvention": { ++ "enum": [ ++ "Legacy", ++ "Compatible" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageSavingConvention" ++ } ++ ], ++ "description": "Gets or sets the image saving convention." ++ }, ++ "MetadataOptions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MetadataOptions" ++ } ++ }, ++ "SkipDeserializationForBasicTypes": { ++ "type": "boolean" ++ }, ++ "ServerName": { ++ "type": "string" ++ }, ++ "UICulture": { ++ "type": "string" ++ }, ++ "SaveMetadataHidden": { ++ "type": "boolean" ++ }, ++ "ContentTypes": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/NameValuePair" ++ } ++ }, ++ "RemoteClientBitrateLimit": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "EnableFolderView": { ++ "type": "boolean" ++ }, ++ "EnableGroupingMoviesIntoCollections": { ++ "type": "boolean" ++ }, ++ "EnableGroupingShowsIntoCollections": { ++ "type": "boolean" ++ }, ++ "DisplaySpecialsWithinSeasons": { ++ "type": "boolean" ++ }, ++ "CodecsUsed": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ } ++ }, ++ "PluginRepositories": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/RepositoryInfo" ++ } ++ }, ++ "EnableExternalContentInSuggestions": { ++ "type": "boolean" ++ }, ++ "ImageExtractionTimeoutMs": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "PathSubstitutions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PathSubstitution" ++ } ++ }, ++ "EnableSlowResponseWarning": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether slow server responses should be logged as a warning." ++ }, ++ "SlowResponseThresholdMs": { ++ "type": "integer", ++ "description": "Gets or sets the threshold for the slow response time warning in ms.", ++ "format": "int64" ++ }, ++ "CorsHosts": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the cors hosts." ++ }, ++ "ActivityLogRetentionDays": { ++ "type": "integer", ++ "description": "Gets or sets the number of days we should retain activity logs.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "LibraryScanFanoutConcurrency": { ++ "type": "integer", ++ "description": "Gets or sets the how the library scan fans out.", ++ "format": "int32" ++ }, ++ "LibraryMetadataRefreshConcurrency": { ++ "type": "integer", ++ "description": "Gets or sets the how many metadata refreshes can run concurrently.", ++ "format": "int32" ++ }, ++ "AllowClientLogUpload": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether clients should be allowed to upload logs." ++ }, ++ "DummyChapterDuration": { ++ "type": "integer", ++ "description": "Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation altogether.", ++ "format": "int32" ++ }, ++ "ChapterImageResolution": { ++ "enum": [ ++ "MatchSource", ++ "P144", ++ "P240", ++ "P360", ++ "P480", ++ "P720", ++ "P1080", ++ "P1440", ++ "P2160" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ImageResolution" ++ } ++ ], ++ "description": "Gets or sets the chapter image resolution." ++ }, ++ "ParallelImageEncodingLimit": { ++ "type": "integer", ++ "description": "Gets or sets the limit for parallel image encoding.", ++ "format": "int32" ++ }, ++ "CastReceiverApplications": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CastReceiverApplication" ++ }, ++ "description": "Gets or sets the list of cast receiver applications." ++ }, ++ "TrickplayOptions": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TrickplayOptions" ++ } ++ ], ++ "description": "Gets or sets the trickplay options." ++ }, ++ "EnableLegacyAuthorization": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether old authorization methods are allowed." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Represents the server configuration." ++ }, ++ "ServerDiscoveryInfo": { ++ "type": "object", ++ "properties": { ++ "Address": { ++ "type": "string", ++ "description": "Gets the address." ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets the server identifier." ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets the name." ++ }, ++ "EndpointAddress": { ++ "type": "string", ++ "description": "Gets the endpoint address.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The server discovery info model." ++ }, ++ "ServerRestartingMessage": { ++ "type": "object", ++ "properties": { ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "ServerRestarting", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Server restarting down message." ++ }, ++ "ServerShuttingDownMessage": { ++ "type": "object", ++ "properties": { ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "ServerShuttingDown", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Server shutting down message." ++ }, ++ "SessionInfoDto": { ++ "type": "object", ++ "properties": { ++ "PlayState": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayerStateInfo" ++ } ++ ], ++ "description": "Gets or sets the play state.", ++ "nullable": true ++ }, ++ "AdditionalUsers": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SessionUserInfo" ++ }, ++ "description": "Gets or sets the additional users.", ++ "nullable": true ++ }, ++ "Capabilities": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ClientCapabilitiesDto" ++ } ++ ], ++ "description": "Gets or sets the client capabilities.", ++ "nullable": true ++ }, ++ "RemoteEndPoint": { ++ "type": "string", ++ "description": "Gets or sets the remote end point.", ++ "nullable": true ++ }, ++ "PlayableMediaTypes": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/MediaType" ++ }, ++ "description": "Gets or sets the playable media types." ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the id.", ++ "nullable": true ++ }, ++ "UserId": { ++ "type": "string", ++ "description": "Gets or sets the user id.", ++ "format": "uuid" ++ }, ++ "UserName": { ++ "type": "string", ++ "description": "Gets or sets the username.", ++ "nullable": true ++ }, ++ "Client": { ++ "type": "string", ++ "description": "Gets or sets the type of the client.", ++ "nullable": true ++ }, ++ "LastActivityDate": { ++ "type": "string", ++ "description": "Gets or sets the last activity date.", ++ "format": "date-time" ++ }, ++ "LastPlaybackCheckIn": { ++ "type": "string", ++ "description": "Gets or sets the last playback check in.", ++ "format": "date-time" ++ }, ++ "LastPausedDate": { ++ "type": "string", ++ "description": "Gets or sets the last paused date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "DeviceName": { ++ "type": "string", ++ "description": "Gets or sets the name of the device.", ++ "nullable": true ++ }, ++ "DeviceType": { ++ "type": "string", ++ "description": "Gets or sets the type of the device.", ++ "nullable": true ++ }, ++ "NowPlayingItem": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ ], ++ "description": "Gets or sets the now playing item.", ++ "nullable": true ++ }, ++ "NowViewingItem": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ ], ++ "description": "Gets or sets the now viewing item.", ++ "nullable": true ++ }, ++ "DeviceId": { ++ "type": "string", ++ "description": "Gets or sets the device id.", ++ "nullable": true ++ }, ++ "ApplicationVersion": { ++ "type": "string", ++ "description": "Gets or sets the application version.", ++ "nullable": true ++ }, ++ "TranscodingInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TranscodingInfo" ++ } ++ ], ++ "description": "Gets or sets the transcoding info.", ++ "nullable": true ++ }, ++ "IsActive": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this session is active." ++ }, ++ "SupportsMediaControl": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the session supports media control." ++ }, ++ "SupportsRemoteControl": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the session supports remote control." ++ }, ++ "NowPlayingQueue": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/QueueItem" ++ }, ++ "description": "Gets or sets the now playing queue.", ++ "nullable": true ++ }, ++ "NowPlayingQueueFullItems": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ }, ++ "description": "Gets or sets the now playing queue full items.", ++ "nullable": true ++ }, ++ "HasCustomDeviceName": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the session has a custom device name." ++ }, ++ "PlaylistItemId": { ++ "type": "string", ++ "description": "Gets or sets the playlist item id.", ++ "nullable": true ++ }, ++ "ServerId": { ++ "type": "string", ++ "description": "Gets or sets the server id.", ++ "nullable": true ++ }, ++ "UserPrimaryImageTag": { ++ "type": "string", ++ "description": "Gets or sets the user primary image tag.", ++ "nullable": true ++ }, ++ "SupportedCommands": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/GeneralCommandType" ++ }, ++ "description": "Gets or sets the supported commands." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Session info DTO." ++ }, ++ "SessionMessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "type": "string", ++ "description": "The different kinds of messages that are used in the WebSocket api." ++ }, ++ "SessionsMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/SessionInfoDto" ++ }, ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "Sessions", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Sessions message." ++ }, ++ "SessionsStartMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "type": "string", ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "SessionsStart", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Sessions start message.\r\nData is the timing data encoded as \"$initialDelay,$interval\" in ms." ++ }, ++ "SessionsStopMessage": { ++ "type": "object", ++ "properties": { ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "SessionsStop", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Sessions stop message." ++ }, ++ "SessionUserInfo": { ++ "type": "object", ++ "properties": { ++ "UserId": { ++ "type": "string", ++ "description": "Gets or sets the user identifier.", ++ "format": "uuid" ++ }, ++ "UserName": { ++ "type": "string", ++ "description": "Gets or sets the name of the user.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class SessionUserInfo." ++ }, ++ "SetChannelMappingDto": { ++ "required": [ ++ "ProviderChannelId", ++ "ProviderId", ++ "TunerChannelId" ++ ], ++ "type": "object", ++ "properties": { ++ "ProviderId": { ++ "type": "string", ++ "description": "Gets or sets the provider id." ++ }, ++ "TunerChannelId": { ++ "type": "string", ++ "description": "Gets or sets the tuner channel id." ++ }, ++ "ProviderChannelId": { ++ "type": "string", ++ "description": "Gets or sets the provider channel id." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Set channel mapping dto." ++ }, ++ "SetPlaylistItemRequestDto": { ++ "type": "object", ++ "properties": { ++ "PlaylistItemId": { ++ "type": "string", ++ "description": "Gets or sets the playlist identifier of the playing item.", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class SetPlaylistItemRequestDto." ++ }, ++ "SetRepeatModeRequestDto": { ++ "type": "object", ++ "properties": { ++ "Mode": { ++ "enum": [ ++ "RepeatOne", ++ "RepeatAll", ++ "RepeatNone" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupRepeatMode" ++ } ++ ], ++ "description": "Enum GroupRepeatMode." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class SetRepeatModeRequestDto." ++ }, ++ "SetShuffleModeRequestDto": { ++ "type": "object", ++ "properties": { ++ "Mode": { ++ "enum": [ ++ "Sorted", ++ "Shuffle" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupShuffleMode" ++ } ++ ], ++ "description": "Enum GroupShuffleMode." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class SetShuffleModeRequestDto." ++ }, ++ "SongInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "description": "Gets or sets the original title.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "MetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "Year": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsAutomated": { ++ "type": "boolean" ++ }, ++ "AlbumArtists": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "Album": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Artists": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SortOrder": { ++ "enum": [ ++ "Ascending", ++ "Descending" ++ ], ++ "type": "string", ++ "description": "An enum representing the sorting order." ++ }, ++ "SpecialViewOptionDto": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets view option name.", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets view option id.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Special view option dto." ++ }, ++ "StartupConfigurationDto": { ++ "type": "object", ++ "properties": { ++ "ServerName": { ++ "type": "string", ++ "description": "Gets or sets the server name.", ++ "nullable": true ++ }, ++ "UICulture": { ++ "type": "string", ++ "description": "Gets or sets UI language culture.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "PreferredMetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the preferred language for the metadata.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The startup configuration DTO." ++ }, ++ "StartupRemoteAccessDto": { ++ "required": [ ++ "EnableAutomaticPortMapping", ++ "EnableRemoteAccess" ++ ], ++ "type": "object", ++ "properties": { ++ "EnableRemoteAccess": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether enable remote access." ++ }, ++ "EnableAutomaticPortMapping": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether enable automatic port mapping.", ++ "deprecated": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Startup remote access dto." ++ }, ++ "StartupUserDto": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the username.", ++ "nullable": true ++ }, ++ "Password": { ++ "type": "string", ++ "description": "Gets or sets the user's password.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The startup user DTO." ++ }, ++ "SubtitleDeliveryMethod": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "type": "string", ++ "description": "Delivery method to use during playback of a specific subtitle format." ++ }, ++ "SubtitleOptions": { ++ "type": "object", ++ "properties": { ++ "SkipIfEmbeddedSubtitlesPresent": { ++ "type": "boolean" ++ }, ++ "SkipIfAudioTrackMatches": { ++ "type": "boolean" ++ }, ++ "DownloadLanguages": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "DownloadMovieSubtitles": { ++ "type": "boolean" ++ }, ++ "DownloadEpisodeSubtitles": { ++ "type": "boolean" ++ }, ++ "OpenSubtitlesUsername": { ++ "type": "string", ++ "nullable": true ++ }, ++ "OpenSubtitlesPasswordHash": { ++ "type": "string", ++ "nullable": true ++ }, ++ "IsOpenSubtitleVipAccount": { ++ "type": "boolean" ++ }, ++ "RequirePerfectMatch": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SubtitlePlaybackMode": { ++ "enum": [ ++ "Default", ++ "Always", ++ "OnlyForced", ++ "None", ++ "Smart" ++ ], ++ "type": "string", ++ "description": "An enum representing a subtitle playback mode." ++ }, ++ "SubtitleProfile": { ++ "type": "object", ++ "properties": { ++ "Format": { ++ "type": "string", ++ "description": "Gets or sets the format.", ++ "nullable": true ++ }, ++ "Method": { ++ "enum": [ ++ "Encode", ++ "Embed", ++ "External", ++ "Hls", ++ "Drop" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitleDeliveryMethod" ++ } ++ ], ++ "description": "Gets or sets the delivery method." ++ }, ++ "DidlMode": { ++ "type": "string", ++ "description": "Gets or sets the DIDL mode.", ++ "nullable": true ++ }, ++ "Language": { ++ "type": "string", ++ "description": "Gets or sets the language.", ++ "nullable": true ++ }, ++ "Container": { ++ "type": "string", ++ "description": "Gets or sets the container.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "A class for subtitle profile information." ++ }, ++ "SyncPlayCommandMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SendCommand" ++ } ++ ], ++ "description": "Class SendCommand.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "SyncPlayCommand", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Sync play command." ++ }, ++ "SyncPlayGroupDoesNotExistUpdate": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid", ++ "readOnly": true ++ }, ++ "Data": { ++ "type": "string", ++ "description": "Gets the update data.", ++ "readOnly": true ++ }, ++ "Type": { ++ "enum": [ ++ "UserJoined", ++ "UserLeft", ++ "GroupJoined", ++ "GroupLeft", ++ "StateUpdate", ++ "PlayQueue", ++ "NotInGroup", ++ "GroupDoesNotExist", ++ "LibraryAccessDenied" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupUpdateType" ++ } ++ ], ++ "description": "Enum GroupUpdateType.", ++ "default": "GroupDoesNotExist", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SyncPlayGroupJoinedUpdate": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid", ++ "readOnly": true ++ }, ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupInfoDto" ++ } ++ ], ++ "description": "Gets the update data.", ++ "readOnly": true ++ }, ++ "Type": { ++ "enum": [ ++ "UserJoined", ++ "UserLeft", ++ "GroupJoined", ++ "GroupLeft", ++ "StateUpdate", ++ "PlayQueue", ++ "NotInGroup", ++ "GroupDoesNotExist", ++ "LibraryAccessDenied" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupUpdateType" ++ } ++ ], ++ "description": "Enum GroupUpdateType.", ++ "default": "GroupJoined", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SyncPlayGroupLeftUpdate": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid", ++ "readOnly": true ++ }, ++ "Data": { ++ "type": "string", ++ "description": "Gets the update data.", ++ "readOnly": true ++ }, ++ "Type": { ++ "enum": [ ++ "UserJoined", ++ "UserLeft", ++ "GroupJoined", ++ "GroupLeft", ++ "StateUpdate", ++ "PlayQueue", ++ "NotInGroup", ++ "GroupDoesNotExist", ++ "LibraryAccessDenied" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupUpdateType" ++ } ++ ], ++ "description": "Enum GroupUpdateType.", ++ "default": "GroupLeft", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SyncPlayGroupUpdateMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupUpdate" ++ } ++ ], ++ "description": "Group update data" ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "SyncPlayGroupUpdate", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Untyped sync play command." ++ }, ++ "SyncPlayLibraryAccessDeniedUpdate": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid", ++ "readOnly": true ++ }, ++ "Data": { ++ "type": "string", ++ "description": "Gets the update data.", ++ "readOnly": true ++ }, ++ "Type": { ++ "enum": [ ++ "UserJoined", ++ "UserLeft", ++ "GroupJoined", ++ "GroupLeft", ++ "StateUpdate", ++ "PlayQueue", ++ "NotInGroup", ++ "GroupDoesNotExist", ++ "LibraryAccessDenied" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupUpdateType" ++ } ++ ], ++ "description": "Enum GroupUpdateType.", ++ "default": "LibraryAccessDenied", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SyncPlayNotInGroupUpdate": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid", ++ "readOnly": true ++ }, ++ "Data": { ++ "type": "string", ++ "description": "Gets the update data.", ++ "readOnly": true ++ }, ++ "Type": { ++ "enum": [ ++ "UserJoined", ++ "UserLeft", ++ "GroupJoined", ++ "GroupLeft", ++ "StateUpdate", ++ "PlayQueue", ++ "NotInGroup", ++ "GroupDoesNotExist", ++ "LibraryAccessDenied" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupUpdateType" ++ } ++ ], ++ "description": "Enum GroupUpdateType.", ++ "default": "NotInGroup", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SyncPlayPlayQueueUpdate": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid", ++ "readOnly": true ++ }, ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/PlayQueueUpdate" ++ } ++ ], ++ "description": "Gets the update data.", ++ "readOnly": true ++ }, ++ "Type": { ++ "enum": [ ++ "UserJoined", ++ "UserLeft", ++ "GroupJoined", ++ "GroupLeft", ++ "StateUpdate", ++ "PlayQueue", ++ "NotInGroup", ++ "GroupDoesNotExist", ++ "LibraryAccessDenied" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupUpdateType" ++ } ++ ], ++ "description": "Enum GroupUpdateType.", ++ "default": "PlayQueue", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SyncPlayQueueItem": { ++ "type": "object", ++ "properties": { ++ "ItemId": { ++ "type": "string", ++ "description": "Gets the item identifier.", ++ "format": "uuid" ++ }, ++ "PlaylistItemId": { ++ "type": "string", ++ "description": "Gets the playlist identifier of the item.", ++ "format": "uuid", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class QueueItem." ++ }, ++ "SyncPlayStateUpdate": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid", ++ "readOnly": true ++ }, ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupStateUpdate" ++ } ++ ], ++ "description": "Gets the update data.", ++ "readOnly": true ++ }, ++ "Type": { ++ "enum": [ ++ "UserJoined", ++ "UserLeft", ++ "GroupJoined", ++ "GroupLeft", ++ "StateUpdate", ++ "PlayQueue", ++ "NotInGroup", ++ "GroupDoesNotExist", ++ "LibraryAccessDenied" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupUpdateType" ++ } ++ ], ++ "description": "Enum GroupUpdateType.", ++ "default": "StateUpdate", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SyncPlayUserAccessType": { ++ "enum": [ ++ "CreateAndJoinGroups", ++ "JoinGroups", ++ "None" ++ ], ++ "type": "string", ++ "description": "Enum SyncPlayUserAccessType." ++ }, ++ "SyncPlayUserJoinedUpdate": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid", ++ "readOnly": true ++ }, ++ "Data": { ++ "type": "string", ++ "description": "Gets the update data.", ++ "readOnly": true ++ }, ++ "Type": { ++ "enum": [ ++ "UserJoined", ++ "UserLeft", ++ "GroupJoined", ++ "GroupLeft", ++ "StateUpdate", ++ "PlayQueue", ++ "NotInGroup", ++ "GroupDoesNotExist", ++ "LibraryAccessDenied" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupUpdateType" ++ } ++ ], ++ "description": "Enum GroupUpdateType.", ++ "default": "UserJoined", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SyncPlayUserLeftUpdate": { ++ "type": "object", ++ "properties": { ++ "GroupId": { ++ "type": "string", ++ "description": "Gets the group identifier.", ++ "format": "uuid", ++ "readOnly": true ++ }, ++ "Data": { ++ "type": "string", ++ "description": "Gets the update data.", ++ "readOnly": true ++ }, ++ "Type": { ++ "enum": [ ++ "UserJoined", ++ "UserLeft", ++ "GroupJoined", ++ "GroupLeft", ++ "StateUpdate", ++ "PlayQueue", ++ "NotInGroup", ++ "GroupDoesNotExist", ++ "LibraryAccessDenied" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/GroupUpdateType" ++ } ++ ], ++ "description": "Enum GroupUpdateType.", ++ "default": "UserLeft", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "SystemInfo": { ++ "type": "object", ++ "properties": { ++ "LocalAddress": { ++ "type": "string", ++ "description": "Gets or sets the local address.", ++ "nullable": true ++ }, ++ "ServerName": { ++ "type": "string", ++ "description": "Gets or sets the name of the server.", ++ "nullable": true ++ }, ++ "Version": { ++ "type": "string", ++ "description": "Gets or sets the server version.", ++ "nullable": true ++ }, ++ "ProductName": { ++ "type": "string", ++ "description": "Gets or sets the product name. This is the AssemblyProduct name.", ++ "nullable": true ++ }, ++ "OperatingSystem": { ++ "type": "string", ++ "description": "Gets or sets the operating system.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the id.", ++ "nullable": true ++ }, ++ "StartupWizardCompleted": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the startup wizard is completed.", ++ "nullable": true ++ }, ++ "OperatingSystemDisplayName": { ++ "type": "string", ++ "description": "Gets or sets the display name of the operating system.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "PackageName": { ++ "type": "string", ++ "description": "Gets or sets the package name.", ++ "nullable": true ++ }, ++ "HasPendingRestart": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance has pending restart." ++ }, ++ "IsShuttingDown": { ++ "type": "boolean" ++ }, ++ "SupportsLibraryMonitor": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [supports library monitor]." ++ }, ++ "WebSocketPortNumber": { ++ "type": "integer", ++ "description": "Gets or sets the web socket port number.", ++ "format": "int32" ++ }, ++ "CompletedInstallations": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/InstallationInfo" ++ }, ++ "description": "Gets or sets the completed installations.", ++ "nullable": true ++ }, ++ "CanSelfRestart": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance can self restart.", ++ "default": true, ++ "deprecated": true ++ }, ++ "CanLaunchWebBrowser": { ++ "type": "boolean", ++ "default": false, ++ "deprecated": true ++ }, ++ "ProgramDataPath": { ++ "type": "string", ++ "description": "Gets or sets the program data path.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "WebPath": { ++ "type": "string", ++ "description": "Gets or sets the web UI resources path.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "ItemsByNamePath": { ++ "type": "string", ++ "description": "Gets or sets the items by name path.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "CachePath": { ++ "type": "string", ++ "description": "Gets or sets the cache path.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "LogPath": { ++ "type": "string", ++ "description": "Gets or sets the log path.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "InternalMetadataPath": { ++ "type": "string", ++ "description": "Gets or sets the internal metadata path.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "TranscodingTempPath": { ++ "type": "string", ++ "description": "Gets or sets the transcode path.", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "CastReceiverApplications": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/CastReceiverApplication" ++ }, ++ "description": "Gets or sets the list of cast receiver applications.", ++ "nullable": true ++ }, ++ "HasUpdateAvailable": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance has update available.", ++ "default": false, ++ "deprecated": true ++ }, ++ "EncoderLocation": { ++ "type": "string", ++ "default": "System", ++ "nullable": true, ++ "deprecated": true ++ }, ++ "SystemArchitecture": { ++ "type": "string", ++ "default": "X64", ++ "nullable": true, ++ "deprecated": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class SystemInfo." ++ }, ++ "SystemStorageDto": { ++ "type": "object", ++ "properties": { ++ "ProgramDataFolder": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/FolderStorageDto" ++ } ++ ], ++ "description": "Gets or sets the Storage information of the program data folder." ++ }, ++ "WebFolder": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/FolderStorageDto" ++ } ++ ], ++ "description": "Gets or sets the Storage information of the web UI resources folder." ++ }, ++ "ImageCacheFolder": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/FolderStorageDto" ++ } ++ ], ++ "description": "Gets or sets the Storage information of the folder where images are cached." ++ }, ++ "CacheFolder": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/FolderStorageDto" ++ } ++ ], ++ "description": "Gets or sets the Storage information of the cache folder." ++ }, ++ "LogFolder": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/FolderStorageDto" ++ } ++ ], ++ "description": "Gets or sets the Storage information of the folder where logfiles are saved to." ++ }, ++ "InternalMetadataFolder": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/FolderStorageDto" ++ } ++ ], ++ "description": "Gets or sets the Storage information of the folder where metadata is stored." ++ }, ++ "TranscodingTempFolder": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/FolderStorageDto" ++ } ++ ], ++ "description": "Gets or sets the Storage information of the transcoding cache." ++ }, ++ "Libraries": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/LibraryStorageDto" ++ }, ++ "description": "Gets or sets the storage informations of all libraries." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Contains informations about the systems storage." ++ }, ++ "TaskCompletionStatus": { ++ "enum": [ ++ "Completed", ++ "Failed", ++ "Cancelled", ++ "Aborted" ++ ], ++ "type": "string", ++ "description": "Enum TaskCompletionStatus." ++ }, ++ "TaskInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "State": { ++ "enum": [ ++ "Idle", ++ "Cancelling", ++ "Running" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TaskState" ++ } ++ ], ++ "description": "Gets or sets the state of the task." ++ }, ++ "CurrentProgressPercentage": { ++ "type": "number", ++ "description": "Gets or sets the progress.", ++ "format": "double", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the id.", ++ "nullable": true ++ }, ++ "LastExecutionResult": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TaskResult" ++ } ++ ], ++ "description": "Gets or sets the last execution result.", ++ "nullable": true ++ }, ++ "Triggers": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TaskTriggerInfo" ++ }, ++ "description": "Gets or sets the triggers.", ++ "nullable": true ++ }, ++ "Description": { ++ "type": "string", ++ "description": "Gets or sets the description.", ++ "nullable": true ++ }, ++ "Category": { ++ "type": "string", ++ "description": "Gets or sets the category.", ++ "nullable": true ++ }, ++ "IsHidden": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is hidden." ++ }, ++ "Key": { ++ "type": "string", ++ "description": "Gets or sets the key.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class TaskInfo." ++ }, ++ "TaskResult": { ++ "type": "object", ++ "properties": { ++ "StartTimeUtc": { ++ "type": "string", ++ "description": "Gets or sets the start time UTC.", ++ "format": "date-time" ++ }, ++ "EndTimeUtc": { ++ "type": "string", ++ "description": "Gets or sets the end time UTC.", ++ "format": "date-time" ++ }, ++ "Status": { ++ "enum": [ ++ "Completed", ++ "Failed", ++ "Cancelled", ++ "Aborted" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TaskCompletionStatus" ++ } ++ ], ++ "description": "Gets or sets the status." ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "Key": { ++ "type": "string", ++ "description": "Gets or sets the key.", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the id.", ++ "nullable": true ++ }, ++ "ErrorMessage": { ++ "type": "string", ++ "description": "Gets or sets the error message.", ++ "nullable": true ++ }, ++ "LongErrorMessage": { ++ "type": "string", ++ "description": "Gets or sets the long error message.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class TaskExecutionInfo." ++ }, ++ "TaskState": { ++ "enum": [ ++ "Idle", ++ "Cancelling", ++ "Running" ++ ], ++ "type": "string", ++ "description": "Enum TaskState." ++ }, ++ "TaskTriggerInfo": { ++ "type": "object", ++ "properties": { ++ "Type": { ++ "enum": [ ++ "DailyTrigger", ++ "WeeklyTrigger", ++ "IntervalTrigger", ++ "StartupTrigger" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TaskTriggerInfoType" ++ } ++ ], ++ "description": "Gets or sets the type." ++ }, ++ "TimeOfDayTicks": { ++ "type": "integer", ++ "description": "Gets or sets the time of day.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "IntervalTicks": { ++ "type": "integer", ++ "description": "Gets or sets the interval.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "DayOfWeek": { ++ "enum": [ ++ "Sunday", ++ "Monday", ++ "Tuesday", ++ "Wednesday", ++ "Thursday", ++ "Friday", ++ "Saturday" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DayOfWeek" ++ } ++ ], ++ "description": "Gets or sets the day of week.", ++ "nullable": true ++ }, ++ "MaxRuntimeTicks": { ++ "type": "integer", ++ "description": "Gets or sets the maximum runtime ticks.", ++ "format": "int64", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class TaskTriggerInfo." ++ }, ++ "TaskTriggerInfoType": { ++ "enum": [ ++ "DailyTrigger", ++ "WeeklyTrigger", ++ "IntervalTrigger", ++ "StartupTrigger" ++ ], ++ "type": "string", ++ "description": "Enum TaskTriggerInfoType." ++ }, ++ "ThemeMediaResult": { ++ "type": "object", ++ "properties": { ++ "Items": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/BaseItemDto" ++ }, ++ "description": "Gets or sets the items." ++ }, ++ "TotalRecordCount": { ++ "type": "integer", ++ "description": "Gets or sets the total number of records available.", ++ "format": "int32" ++ }, ++ "StartIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the first record in Items.", ++ "format": "int32" ++ }, ++ "OwnerId": { ++ "type": "string", ++ "description": "Gets or sets the owner id.", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class ThemeMediaResult." ++ }, ++ "TimerCancelledMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TimerEventInfo" ++ } ++ ], ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "TimerCancelled", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Timer cancelled message." ++ }, ++ "TimerCreatedMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TimerEventInfo" ++ } ++ ], ++ "description": "Gets or sets the data.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "TimerCreated", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Timer created message." ++ }, ++ "TimerEventInfo": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string" ++ }, ++ "ProgramId": { ++ "type": "string", ++ "format": "uuid", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "TimerInfoDto": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the Id of the recording.", ++ "nullable": true ++ }, ++ "Type": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ServerId": { ++ "type": "string", ++ "description": "Gets or sets the server identifier.", ++ "nullable": true ++ }, ++ "ExternalId": { ++ "type": "string", ++ "description": "Gets or sets the external identifier.", ++ "nullable": true ++ }, ++ "ChannelId": { ++ "type": "string", ++ "description": "Gets or sets the channel id of the recording.", ++ "format": "uuid" ++ }, ++ "ExternalChannelId": { ++ "type": "string", ++ "description": "Gets or sets the external channel identifier.", ++ "nullable": true ++ }, ++ "ChannelName": { ++ "type": "string", ++ "description": "Gets or sets the channel name of the recording.", ++ "nullable": true ++ }, ++ "ChannelPrimaryImageTag": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ProgramId": { ++ "type": "string", ++ "description": "Gets or sets the program identifier.", ++ "nullable": true ++ }, ++ "ExternalProgramId": { ++ "type": "string", ++ "description": "Gets or sets the external program identifier.", ++ "nullable": true ++ }, ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name of the recording.", ++ "nullable": true ++ }, ++ "Overview": { ++ "type": "string", ++ "description": "Gets or sets the description of the recording.", ++ "nullable": true ++ }, ++ "StartDate": { ++ "type": "string", ++ "description": "Gets or sets the start date of the recording, in UTC.", ++ "format": "date-time" ++ }, ++ "EndDate": { ++ "type": "string", ++ "description": "Gets or sets the end date of the recording, in UTC.", ++ "format": "date-time" ++ }, ++ "ServiceName": { ++ "type": "string", ++ "description": "Gets or sets the name of the service.", ++ "nullable": true ++ }, ++ "Priority": { ++ "type": "integer", ++ "description": "Gets or sets the priority.", ++ "format": "int32" ++ }, ++ "PrePaddingSeconds": { ++ "type": "integer", ++ "description": "Gets or sets the pre padding seconds.", ++ "format": "int32" ++ }, ++ "PostPaddingSeconds": { ++ "type": "integer", ++ "description": "Gets or sets the post padding seconds.", ++ "format": "int32" ++ }, ++ "IsPrePaddingRequired": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is pre padding required." ++ }, ++ "ParentBackdropItemId": { ++ "type": "string", ++ "description": "Gets or sets the Id of the Parent that has a backdrop if the item does not have one.", ++ "nullable": true ++ }, ++ "ParentBackdropImageTags": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the parent backdrop image tags.", ++ "nullable": true ++ }, ++ "IsPostPaddingRequired": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is post padding required." ++ }, ++ "KeepUntil": { ++ "enum": [ ++ "UntilDeleted", ++ "UntilSpaceNeeded", ++ "UntilWatched", ++ "UntilDate" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/KeepUntil" ++ } ++ ] ++ }, ++ "Status": { ++ "enum": [ ++ "New", ++ "InProgress", ++ "Completed", ++ "Cancelled", ++ "ConflictedOk", ++ "ConflictedNotOk", ++ "Error" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/RecordingStatus" ++ } ++ ], ++ "description": "Gets or sets the status." ++ }, ++ "SeriesTimerId": { ++ "type": "string", ++ "description": "Gets or sets the series timer identifier.", ++ "nullable": true ++ }, ++ "ExternalSeriesTimerId": { ++ "type": "string", ++ "description": "Gets or sets the external series timer identifier.", ++ "nullable": true ++ }, ++ "RunTimeTicks": { ++ "type": "integer", ++ "description": "Gets or sets the run time ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "ProgramInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/BaseItemDto" ++ } ++ ], ++ "description": "Gets or sets the program information.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "TimerInfoDtoQueryResult": { ++ "type": "object", ++ "properties": { ++ "Items": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TimerInfoDto" ++ }, ++ "description": "Gets or sets the items." ++ }, ++ "TotalRecordCount": { ++ "type": "integer", ++ "description": "Gets or sets the total number of records available.", ++ "format": "int32" ++ }, ++ "StartIndex": { ++ "type": "integer", ++ "description": "Gets or sets the index of the first record in Items.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Query result container." ++ }, ++ "TonemappingAlgorithm": { ++ "enum": [ ++ "none", ++ "clip", ++ "linear", ++ "gamma", ++ "reinhard", ++ "hable", ++ "mobius", ++ "bt2390" ++ ], ++ "type": "string", ++ "description": "Enum containing tonemapping algorithms." ++ }, ++ "TonemappingMode": { ++ "enum": [ ++ "auto", ++ "max", ++ "rgb", ++ "lum", ++ "itp" ++ ], ++ "type": "string", ++ "description": "Enum containing tonemapping modes." ++ }, ++ "TonemappingRange": { ++ "enum": [ ++ "auto", ++ "tv", ++ "pc" ++ ], ++ "type": "string", ++ "description": "Enum containing tonemapping ranges." ++ }, ++ "TrailerInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "OriginalTitle": { ++ "type": "string", ++ "description": "Gets or sets the original title.", ++ "nullable": true ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "MetadataLanguage": { ++ "type": "string", ++ "description": "Gets or sets the metadata language.", ++ "nullable": true ++ }, ++ "MetadataCountryCode": { ++ "type": "string", ++ "description": "Gets or sets the metadata country code.", ++ "nullable": true ++ }, ++ "ProviderIds": { ++ "type": "object", ++ "additionalProperties": { ++ "type": "string", ++ "nullable": true ++ }, ++ "description": "Gets or sets the provider ids.", ++ "nullable": true ++ }, ++ "Year": { ++ "type": "integer", ++ "description": "Gets or sets the year.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "ParentIndexNumber": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PremiereDate": { ++ "type": "string", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "IsAutomated": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "TrailerInfoRemoteSearchQuery": { ++ "type": "object", ++ "properties": { ++ "SearchInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TrailerInfo" ++ } ++ ], ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "SearchProviderName": { ++ "type": "string", ++ "description": "Gets or sets the provider name to search within if set.", ++ "nullable": true ++ }, ++ "IncludeDisabledProviders": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether disabled providers should be included." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "TranscodeReason": { ++ "enum": [ ++ "ContainerNotSupported", ++ "VideoCodecNotSupported", ++ "AudioCodecNotSupported", ++ "SubtitleCodecNotSupported", ++ "AudioIsExternal", ++ "SecondaryAudioNotSupported", ++ "VideoProfileNotSupported", ++ "VideoLevelNotSupported", ++ "VideoResolutionNotSupported", ++ "VideoBitDepthNotSupported", ++ "VideoFramerateNotSupported", ++ "RefFramesNotSupported", ++ "AnamorphicVideoNotSupported", ++ "InterlacedVideoNotSupported", ++ "AudioChannelsNotSupported", ++ "AudioProfileNotSupported", ++ "AudioSampleRateNotSupported", ++ "AudioBitDepthNotSupported", ++ "ContainerBitrateExceedsLimit", ++ "VideoBitrateNotSupported", ++ "AudioBitrateNotSupported", ++ "UnknownVideoStreamInfo", ++ "UnknownAudioStreamInfo", ++ "DirectPlayError", ++ "VideoRangeTypeNotSupported", ++ "VideoCodecTagNotSupported", ++ "StreamCountExceedsLimit" ++ ], ++ "type": "string" ++ }, ++ "TranscodeSeekInfo": { ++ "enum": [ ++ "Auto", ++ "Bytes" ++ ], ++ "type": "string" ++ }, ++ "TranscodingInfo": { ++ "type": "object", ++ "properties": { ++ "AudioCodec": { ++ "type": "string", ++ "description": "Gets or sets the thread count used for encoding.", ++ "nullable": true ++ }, ++ "VideoCodec": { ++ "type": "string", ++ "description": "Gets or sets the thread count used for encoding.", ++ "nullable": true ++ }, ++ "Container": { ++ "type": "string", ++ "description": "Gets or sets the thread count used for encoding.", ++ "nullable": true ++ }, ++ "IsVideoDirect": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the video is passed through." ++ }, ++ "IsAudioDirect": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the audio is passed through." ++ }, ++ "Bitrate": { ++ "type": "integer", ++ "description": "Gets or sets the bitrate.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Framerate": { ++ "type": "number", ++ "description": "Gets or sets the framerate.", ++ "format": "float", ++ "nullable": true ++ }, ++ "CompletionPercentage": { ++ "type": "number", ++ "description": "Gets or sets the completion percentage.", ++ "format": "double", ++ "nullable": true ++ }, ++ "Width": { ++ "type": "integer", ++ "description": "Gets or sets the video width.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "Height": { ++ "type": "integer", ++ "description": "Gets or sets the video height.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "AudioChannels": { ++ "type": "integer", ++ "description": "Gets or sets the audio channels.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "HardwareAccelerationType": { ++ "enum": [ ++ "none", ++ "amf", ++ "qsv", ++ "nvenc", ++ "v4l2m2m", ++ "vaapi", ++ "videotoolbox", ++ "rkmpp" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/HardwareAccelerationType" ++ } ++ ], ++ "description": "Gets or sets the hardware acceleration type.", ++ "nullable": true ++ }, ++ "TranscodeReasons": { ++ "enum": [ ++ "ContainerNotSupported", ++ "VideoCodecNotSupported", ++ "AudioCodecNotSupported", ++ "SubtitleCodecNotSupported", ++ "AudioIsExternal", ++ "SecondaryAudioNotSupported", ++ "VideoProfileNotSupported", ++ "VideoLevelNotSupported", ++ "VideoResolutionNotSupported", ++ "VideoBitDepthNotSupported", ++ "VideoFramerateNotSupported", ++ "RefFramesNotSupported", ++ "AnamorphicVideoNotSupported", ++ "InterlacedVideoNotSupported", ++ "AudioChannelsNotSupported", ++ "AudioProfileNotSupported", ++ "AudioSampleRateNotSupported", ++ "AudioBitDepthNotSupported", ++ "ContainerBitrateExceedsLimit", ++ "VideoBitrateNotSupported", ++ "AudioBitrateNotSupported", ++ "UnknownVideoStreamInfo", ++ "UnknownAudioStreamInfo", ++ "DirectPlayError", ++ "VideoRangeTypeNotSupported", ++ "VideoCodecTagNotSupported", ++ "StreamCountExceedsLimit" ++ ], ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/TranscodeReason" ++ }, ++ "description": "Gets or sets the transcode reasons." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class holding information on a running transcode." ++ }, ++ "TranscodingProfile": { ++ "type": "object", ++ "properties": { ++ "Container": { ++ "type": "string", ++ "description": "Gets or sets the container." ++ }, ++ "Type": { ++ "enum": [ ++ "Audio", ++ "Video", ++ "Photo", ++ "Subtitle", ++ "Lyric" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/DlnaProfileType" ++ } ++ ], ++ "description": "Gets or sets the DLNA profile type." ++ }, ++ "VideoCodec": { ++ "type": "string", ++ "description": "Gets or sets the video codec." ++ }, ++ "AudioCodec": { ++ "type": "string", ++ "description": "Gets or sets the audio codec." ++ }, ++ "Protocol": { ++ "enum": [ ++ "http", ++ "hls" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaStreamProtocol" ++ } ++ ], ++ "description": "Gets or sets the protocol." ++ }, ++ "EstimateContentLength": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the content length should be estimated.", ++ "default": false ++ }, ++ "EnableMpegtsM2TsMode": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether M2TS mode is enabled.", ++ "default": false ++ }, ++ "TranscodeSeekInfo": { ++ "enum": [ ++ "Auto", ++ "Bytes" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TranscodeSeekInfo" ++ } ++ ], ++ "description": "Gets or sets the transcoding seek info mode.", ++ "default": "Auto" ++ }, ++ "CopyTimestamps": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether timestamps should be copied.", ++ "default": false ++ }, ++ "Context": { ++ "enum": [ ++ "Streaming", ++ "Static" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/EncodingContext" ++ } ++ ], ++ "description": "Gets or sets the encoding context.", ++ "default": "Streaming" ++ }, ++ "EnableSubtitlesInManifest": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether subtitles are allowed in the manifest.", ++ "default": false ++ }, ++ "MaxAudioChannels": { ++ "type": "string", ++ "description": "Gets or sets the maximum audio channels.", ++ "nullable": true ++ }, ++ "MinSegments": { ++ "type": "integer", ++ "description": "Gets or sets the minimum amount of segments.", ++ "format": "int32", ++ "default": 0 ++ }, ++ "SegmentLength": { ++ "type": "integer", ++ "description": "Gets or sets the segment length.", ++ "format": "int32", ++ "default": 0 ++ }, ++ "BreakOnNonKeyFrames": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether breaking the video stream on non-keyframes is supported.", ++ "default": false ++ }, ++ "Conditions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ProfileCondition" ++ }, ++ "description": "Gets or sets the profile conditions." ++ }, ++ "EnableAudioVbrEncoding": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether variable bitrate encoding is supported.", ++ "default": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "A class for transcoding profile information.\r\nNote for client developers: Conditions defined in MediaBrowser.Model.Dlna.CodecProfile has higher priority and can override values defined here." ++ }, ++ "TransportStreamTimestamp": { ++ "enum": [ ++ "None", ++ "Zero", ++ "Valid" ++ ], ++ "type": "string" ++ }, ++ "TrickplayInfoDto": { ++ "type": "object", ++ "properties": { ++ "Width": { ++ "type": "integer", ++ "description": "Gets the width of an individual thumbnail.", ++ "format": "int32" ++ }, ++ "Height": { ++ "type": "integer", ++ "description": "Gets the height of an individual thumbnail.", ++ "format": "int32" ++ }, ++ "TileWidth": { ++ "type": "integer", ++ "description": "Gets the amount of thumbnails per row.", ++ "format": "int32" ++ }, ++ "TileHeight": { ++ "type": "integer", ++ "description": "Gets the amount of thumbnails per column.", ++ "format": "int32" ++ }, ++ "ThumbnailCount": { ++ "type": "integer", ++ "description": "Gets the total amount of non-black thumbnails.", ++ "format": "int32" ++ }, ++ "Interval": { ++ "type": "integer", ++ "description": "Gets the interval in milliseconds between each trickplay thumbnail.", ++ "format": "int32" ++ }, ++ "Bandwidth": { ++ "type": "integer", ++ "description": "Gets the peak bandwidth usage in bits per second.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The trickplay api model." ++ }, ++ "TrickplayOptions": { ++ "type": "object", ++ "properties": { ++ "EnableHwAcceleration": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether or not to use HW acceleration." ++ }, ++ "EnableHwEncoding": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether or not to use HW accelerated MJPEG encoding." ++ }, ++ "EnableKeyFrameOnlyExtraction": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to only extract key frames.\r\nSignificantly faster, but is not compatible with all decoders and/or video files." ++ }, ++ "ScanBehavior": { ++ "enum": [ ++ "Blocking", ++ "NonBlocking" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/TrickplayScanBehavior" ++ } ++ ], ++ "description": "Gets or sets the behavior used by trickplay provider on library scan/update." ++ }, ++ "ProcessPriority": { ++ "enum": [ ++ "Normal", ++ "Idle", ++ "High", ++ "RealTime", ++ "BelowNormal", ++ "AboveNormal" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/ProcessPriorityClass" ++ } ++ ], ++ "description": "Gets or sets the process priority for the ffmpeg process." ++ }, ++ "Interval": { ++ "type": "integer", ++ "description": "Gets or sets the interval, in ms, between each new trickplay image.", ++ "format": "int32" ++ }, ++ "WidthResolutions": { ++ "type": "array", ++ "items": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "description": "Gets or sets the target width resolutions, in px, to generates preview images for." ++ }, ++ "TileWidth": { ++ "type": "integer", ++ "description": "Gets or sets number of tile images to allow in X dimension.", ++ "format": "int32" ++ }, ++ "TileHeight": { ++ "type": "integer", ++ "description": "Gets or sets number of tile images to allow in Y dimension.", ++ "format": "int32" ++ }, ++ "Qscale": { ++ "type": "integer", ++ "description": "Gets or sets the ffmpeg output quality level.", ++ "format": "int32" ++ }, ++ "JpegQuality": { ++ "type": "integer", ++ "description": "Gets or sets the jpeg quality to use for image tiles.", ++ "format": "int32" ++ }, ++ "ProcessThreads": { ++ "type": "integer", ++ "description": "Gets or sets the number of threads to be used by ffmpeg.", ++ "format": "int32" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class TrickplayOptions." ++ }, ++ "TrickplayScanBehavior": { ++ "enum": [ ++ "Blocking", ++ "NonBlocking" ++ ], ++ "type": "string", ++ "description": "Enum TrickplayScanBehavior." ++ }, ++ "TunerChannelMapping": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ProviderChannelName": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ProviderChannelId": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "TunerHostInfo": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Url": { ++ "type": "string", ++ "nullable": true ++ }, ++ "Type": { ++ "type": "string", ++ "nullable": true ++ }, ++ "DeviceId": { ++ "type": "string", ++ "nullable": true ++ }, ++ "FriendlyName": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ImportFavoritesOnly": { ++ "type": "boolean" ++ }, ++ "AllowHWTranscoding": { ++ "type": "boolean" ++ }, ++ "AllowFmp4TranscodingContainer": { ++ "type": "boolean" ++ }, ++ "AllowStreamSharing": { ++ "type": "boolean" ++ }, ++ "FallbackMaxStreamingBitrate": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "EnableStreamLooping": { ++ "type": "boolean" ++ }, ++ "Source": { ++ "type": "string", ++ "nullable": true ++ }, ++ "TunerCount": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "UserAgent": { ++ "type": "string", ++ "nullable": true ++ }, ++ "IgnoreDts": { ++ "type": "boolean" ++ }, ++ "ReadAtNativeFramerate": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "TypeOptions": { ++ "type": "object", ++ "properties": { ++ "Type": { ++ "type": "string", ++ "nullable": true ++ }, ++ "MetadataFetchers": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "MetadataFetcherOrder": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "ImageFetchers": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "ImageFetcherOrder": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "ImageOptions": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/ImageOption" ++ }, ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "UnratedItem": { ++ "enum": [ ++ "Movie", ++ "Trailer", ++ "Series", ++ "Music", ++ "Book", ++ "LiveTvChannel", ++ "LiveTvProgram", ++ "ChannelContent", ++ "Other" ++ ], ++ "type": "string", ++ "description": "An enum representing an unrated item." ++ }, ++ "UpdateLibraryOptionsDto": { ++ "type": "object", ++ "properties": { ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the library item id.", ++ "format": "uuid" ++ }, ++ "LibraryOptions": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/LibraryOptions" ++ } ++ ], ++ "description": "Gets or sets library options.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Update library options dto." ++ }, ++ "UpdateMediaPathRequestDto": { ++ "required": [ ++ "Name", ++ "PathInfo" ++ ], ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the library name." ++ }, ++ "PathInfo": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/MediaPathInfo" ++ } ++ ], ++ "description": "Gets or sets library folder path information." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Update library options dto." ++ }, ++ "UpdatePlaylistDto": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name of the new playlist.", ++ "nullable": true ++ }, ++ "Ids": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "description": "Gets or sets item ids of the playlist.", ++ "nullable": true ++ }, ++ "Users": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/PlaylistUserPermissions" ++ }, ++ "description": "Gets or sets the playlist users.", ++ "nullable": true ++ }, ++ "IsPublic": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the playlist is public.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Update existing playlist dto. Fields set to `null` will not be updated and keep their current values." ++ }, ++ "UpdatePlaylistUserDto": { ++ "type": "object", ++ "properties": { ++ "CanEdit": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the user can edit the playlist.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values." ++ }, ++ "UpdateUserItemDataDto": { ++ "type": "object", ++ "properties": { ++ "Rating": { ++ "type": "number", ++ "description": "Gets or sets the rating.", ++ "format": "double", ++ "nullable": true ++ }, ++ "PlayedPercentage": { ++ "type": "number", ++ "description": "Gets or sets the played percentage.", ++ "format": "double", ++ "nullable": true ++ }, ++ "UnplayedItemCount": { ++ "type": "integer", ++ "description": "Gets or sets the unplayed item count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PlaybackPositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the playback position ticks.", ++ "format": "int64", ++ "nullable": true ++ }, ++ "PlayCount": { ++ "type": "integer", ++ "description": "Gets or sets the play count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "IsFavorite": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is favorite.", ++ "nullable": true ++ }, ++ "Likes": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Dto.UpdateUserItemDataDto is likes.", ++ "nullable": true ++ }, ++ "LastPlayedDate": { ++ "type": "string", ++ "description": "Gets or sets the last played date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "Played": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Dto.UserItemDataDto is played.", ++ "nullable": true ++ }, ++ "Key": { ++ "type": "string", ++ "description": "Gets or sets the key.", ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "description": "Gets or sets the item identifier.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "This is used by the api to get information about a item user data." ++ }, ++ "UpdateUserPassword": { ++ "type": "object", ++ "properties": { ++ "CurrentPassword": { ++ "type": "string", ++ "description": "Gets or sets the current sha1-hashed password.", ++ "nullable": true ++ }, ++ "CurrentPw": { ++ "type": "string", ++ "description": "Gets or sets the current plain text password.", ++ "nullable": true ++ }, ++ "NewPw": { ++ "type": "string", ++ "description": "Gets or sets the new plain text password.", ++ "nullable": true ++ }, ++ "ResetPassword": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether to reset the password." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "The update user password request body." ++ }, ++ "UploadSubtitleDto": { ++ "required": [ ++ "Data", ++ "Format", ++ "IsForced", ++ "IsHearingImpaired", ++ "Language" ++ ], ++ "type": "object", ++ "properties": { ++ "Language": { ++ "type": "string", ++ "description": "Gets or sets the subtitle language." ++ }, ++ "Format": { ++ "type": "string", ++ "description": "Gets or sets the subtitle format." ++ }, ++ "IsForced": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the subtitle is forced." ++ }, ++ "IsHearingImpaired": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether the subtitle is for hearing impaired." ++ }, ++ "Data": { ++ "type": "string", ++ "description": "Gets or sets the subtitle data." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Upload subtitles dto." ++ }, ++ "UserConfiguration": { ++ "type": "object", ++ "properties": { ++ "AudioLanguagePreference": { ++ "type": "string", ++ "description": "Gets or sets the audio language preference.", ++ "nullable": true ++ }, ++ "PlayDefaultAudioTrack": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [play default audio track]." ++ }, ++ "SubtitleLanguagePreference": { ++ "type": "string", ++ "description": "Gets or sets the subtitle language preference.", ++ "nullable": true ++ }, ++ "DisplayMissingEpisodes": { ++ "type": "boolean" ++ }, ++ "GroupedFolders": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ "SubtitleMode": { ++ "enum": [ ++ "Default", ++ "Always", ++ "OnlyForced", ++ "None", ++ "Smart" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SubtitlePlaybackMode" ++ } ++ ], ++ "description": "An enum representing a subtitle playback mode." ++ }, ++ "DisplayCollectionsView": { ++ "type": "boolean" ++ }, ++ "EnableLocalPassword": { ++ "type": "boolean" ++ }, ++ "OrderedViews": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ "LatestItemsExcludes": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ "MyMediaExcludes": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ } ++ }, ++ "HidePlayedInLatest": { ++ "type": "boolean" ++ }, ++ "RememberAudioSelections": { ++ "type": "boolean" ++ }, ++ "RememberSubtitleSelections": { ++ "type": "boolean" ++ }, ++ "EnableNextEpisodeAutoPlay": { ++ "type": "boolean" ++ }, ++ "CastReceiverId": { ++ "type": "string", ++ "description": "Gets or sets the id of the selected cast receiver.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class UserConfiguration." ++ }, ++ "UserDataChangedMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserDataChangeInfo" ++ } ++ ], ++ "description": "Class UserDataChangeInfo.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "UserDataChanged", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "User data changed message." ++ }, ++ "UserDataChangeInfo": { ++ "type": "object", ++ "properties": { ++ "UserId": { ++ "type": "string", ++ "description": "Gets or sets the user id.", ++ "format": "uuid" ++ }, ++ "UserDataList": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/UserItemDataDto" ++ }, ++ "description": "Gets or sets the user data list." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class UserDataChangeInfo." ++ }, ++ "UserDeletedMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "type": "string", ++ "description": "Gets or sets the data.", ++ "format": "uuid" ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "UserDeleted", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "User deleted message." ++ }, ++ "UserDto": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "ServerId": { ++ "type": "string", ++ "description": "Gets or sets the server identifier.", ++ "nullable": true ++ }, ++ "ServerName": { ++ "type": "string", ++ "description": "Gets or sets the name of the server.\r\nThis is not used by the server and is for client-side usage only.", ++ "nullable": true ++ }, ++ "Id": { ++ "type": "string", ++ "description": "Gets or sets the id.", ++ "format": "uuid" ++ }, ++ "PrimaryImageTag": { ++ "type": "string", ++ "description": "Gets or sets the primary image tag.", ++ "nullable": true ++ }, ++ "HasPassword": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance has password." ++ }, ++ "HasConfiguredPassword": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance has configured password." ++ }, ++ "HasConfiguredEasyPassword": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance has configured easy password.", ++ "deprecated": true ++ }, ++ "EnableAutoLogin": { ++ "type": "boolean", ++ "description": "Gets or sets whether async login is enabled or not.", ++ "nullable": true ++ }, ++ "LastLoginDate": { ++ "type": "string", ++ "description": "Gets or sets the last login date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "LastActivityDate": { ++ "type": "string", ++ "description": "Gets or sets the last activity date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "Configuration": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserConfiguration" ++ } ++ ], ++ "description": "Gets or sets the configuration.", ++ "nullable": true ++ }, ++ "Policy": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserPolicy" ++ } ++ ], ++ "description": "Gets or sets the policy.", ++ "nullable": true ++ }, ++ "PrimaryImageAspectRatio": { ++ "type": "number", ++ "description": "Gets or sets the primary image aspect ratio.", ++ "format": "double", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class UserDto." ++ }, ++ "UserItemDataDto": { ++ "type": "object", ++ "properties": { ++ "Rating": { ++ "type": "number", ++ "description": "Gets or sets the rating.", ++ "format": "double", ++ "nullable": true ++ }, ++ "PlayedPercentage": { ++ "type": "number", ++ "description": "Gets or sets the played percentage.", ++ "format": "double", ++ "nullable": true ++ }, ++ "UnplayedItemCount": { ++ "type": "integer", ++ "description": "Gets or sets the unplayed item count.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "PlaybackPositionTicks": { ++ "type": "integer", ++ "description": "Gets or sets the playback position ticks.", ++ "format": "int64" ++ }, ++ "PlayCount": { ++ "type": "integer", ++ "description": "Gets or sets the play count.", ++ "format": "int32" ++ }, ++ "IsFavorite": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is favorite." ++ }, ++ "Likes": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Dto.UserItemDataDto is likes.", ++ "nullable": true ++ }, ++ "LastPlayedDate": { ++ "type": "string", ++ "description": "Gets or sets the last played date.", ++ "format": "date-time", ++ "nullable": true ++ }, ++ "Played": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this MediaBrowser.Model.Dto.UserItemDataDto is played." ++ }, ++ "Key": { ++ "type": "string", ++ "description": "Gets or sets the key." ++ }, ++ "ItemId": { ++ "type": "string", ++ "description": "Gets or sets the item identifier.", ++ "format": "uuid" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class UserItemDataDto." ++ }, ++ "UserPolicy": { ++ "required": [ ++ "AuthenticationProviderId", ++ "PasswordResetProviderId" ++ ], ++ "type": "object", ++ "properties": { ++ "IsAdministrator": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is administrator." ++ }, ++ "IsHidden": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is hidden." ++ }, ++ "EnableCollectionManagement": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance can manage collections.", ++ "default": false ++ }, ++ "EnableSubtitleManagement": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance can manage subtitles.", ++ "default": false ++ }, ++ "EnableLyricManagement": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this user can manage lyrics.", ++ "default": false ++ }, ++ "IsDisabled": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether this instance is disabled." ++ }, ++ "MaxParentalRating": { ++ "type": "integer", ++ "description": "Gets or sets the max parental rating.", ++ "format": "int32", ++ "nullable": true ++ }, ++ "MaxParentalSubRating": { ++ "type": "integer", ++ "format": "int32", ++ "nullable": true ++ }, ++ "BlockedTags": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "AllowedTags": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "EnableUserPreferenceAccess": { ++ "type": "boolean" ++ }, ++ "AccessSchedules": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/AccessSchedule" ++ }, ++ "nullable": true ++ }, ++ "BlockUnratedItems": { ++ "type": "array", ++ "items": { ++ "$ref": "#/components/schemas/UnratedItem" ++ }, ++ "nullable": true ++ }, ++ "EnableRemoteControlOfOtherUsers": { ++ "type": "boolean" ++ }, ++ "EnableSharedDeviceControl": { ++ "type": "boolean" ++ }, ++ "EnableRemoteAccess": { ++ "type": "boolean" ++ }, ++ "EnableLiveTvManagement": { ++ "type": "boolean" ++ }, ++ "EnableLiveTvAccess": { ++ "type": "boolean" ++ }, ++ "EnableMediaPlayback": { ++ "type": "boolean" ++ }, ++ "EnableAudioPlaybackTranscoding": { ++ "type": "boolean" ++ }, ++ "EnableVideoPlaybackTranscoding": { ++ "type": "boolean" ++ }, ++ "EnablePlaybackRemuxing": { ++ "type": "boolean" ++ }, ++ "ForceRemoteSourceTranscoding": { ++ "type": "boolean" ++ }, ++ "EnableContentDeletion": { ++ "type": "boolean" ++ }, ++ "EnableContentDeletionFromFolders": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "EnableContentDownloading": { ++ "type": "boolean" ++ }, ++ "EnableSyncTranscoding": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether [enable synchronize]." ++ }, ++ "EnableMediaConversion": { ++ "type": "boolean" ++ }, ++ "EnabledDevices": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "nullable": true ++ }, ++ "EnableAllDevices": { ++ "type": "boolean" ++ }, ++ "EnabledChannels": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "nullable": true ++ }, ++ "EnableAllChannels": { ++ "type": "boolean" ++ }, ++ "EnabledFolders": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "nullable": true ++ }, ++ "EnableAllFolders": { ++ "type": "boolean" ++ }, ++ "InvalidLoginAttemptCount": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "LoginAttemptsBeforeLockout": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "MaxActiveSessions": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "EnablePublicSharing": { ++ "type": "boolean" ++ }, ++ "BlockedMediaFolders": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "nullable": true ++ }, ++ "BlockedChannels": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "format": "uuid" ++ }, ++ "nullable": true ++ }, ++ "RemoteClientBitrateLimit": { ++ "type": "integer", ++ "format": "int32" ++ }, ++ "AuthenticationProviderId": { ++ "type": "string" ++ }, ++ "PasswordResetProviderId": { ++ "type": "string" ++ }, ++ "SyncPlayAccess": { ++ "enum": [ ++ "CreateAndJoinGroups", ++ "JoinGroups", ++ "None" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SyncPlayUserAccessType" ++ } ++ ], ++ "description": "Gets or sets a value indicating what SyncPlay features the user can access." ++ } ++ }, ++ "additionalProperties": false ++ }, ++ "UserUpdatedMessage": { ++ "type": "object", ++ "properties": { ++ "Data": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/UserDto" ++ } ++ ], ++ "description": "Class UserDto.", ++ "nullable": true ++ }, ++ "MessageId": { ++ "type": "string", ++ "description": "Gets or sets the message id.", ++ "format": "uuid" ++ }, ++ "MessageType": { ++ "enum": [ ++ "ForceKeepAlive", ++ "GeneralCommand", ++ "UserDataChanged", ++ "Sessions", ++ "Play", ++ "SyncPlayCommand", ++ "SyncPlayGroupUpdate", ++ "Playstate", ++ "RestartRequired", ++ "ServerShuttingDown", ++ "ServerRestarting", ++ "LibraryChanged", ++ "UserDeleted", ++ "UserUpdated", ++ "SeriesTimerCreated", ++ "TimerCreated", ++ "SeriesTimerCancelled", ++ "TimerCancelled", ++ "RefreshProgress", ++ "ScheduledTaskEnded", ++ "PackageInstallationCancelled", ++ "PackageInstallationFailed", ++ "PackageInstallationCompleted", ++ "PackageInstalling", ++ "PackageUninstalled", ++ "ActivityLogEntry", ++ "ScheduledTasksInfo", ++ "ActivityLogEntryStart", ++ "ActivityLogEntryStop", ++ "SessionsStart", ++ "SessionsStop", ++ "ScheduledTasksInfoStart", ++ "ScheduledTasksInfoStop", ++ "KeepAlive" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/SessionMessageType" ++ } ++ ], ++ "description": "The different kinds of messages that are used in the WebSocket api.", ++ "default": "UserUpdated", ++ "readOnly": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "User updated message." ++ }, ++ "UtcTimeResponse": { ++ "type": "object", ++ "properties": { ++ "RequestReceptionTime": { ++ "type": "string", ++ "description": "Gets the UTC time when request has been received.", ++ "format": "date-time" ++ }, ++ "ResponseTransmissionTime": { ++ "type": "string", ++ "description": "Gets the UTC time when response has been sent.", ++ "format": "date-time" ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Class UtcTimeResponse." ++ }, ++ "ValidatePathDto": { ++ "type": "object", ++ "properties": { ++ "ValidateWritable": { ++ "type": "boolean", ++ "description": "Gets or sets a value indicating whether validate if path is writable." ++ }, ++ "Path": { ++ "type": "string", ++ "description": "Gets or sets the path.", ++ "nullable": true ++ }, ++ "IsFile": { ++ "type": "boolean", ++ "description": "Gets or sets is path file.", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Validate path object." ++ }, ++ "VersionInfo": { ++ "type": "object", ++ "properties": { ++ "version": { ++ "type": "string", ++ "description": "Gets or sets the version." ++ }, ++ "VersionNumber": { ++ "type": "string", ++ "description": "Gets the version as a System.Version.", ++ "readOnly": true ++ }, ++ "changelog": { ++ "type": "string", ++ "description": "Gets or sets the changelog for this version.", ++ "nullable": true ++ }, ++ "targetAbi": { ++ "type": "string", ++ "description": "Gets or sets the ABI that this version was built against.", ++ "nullable": true ++ }, ++ "sourceUrl": { ++ "type": "string", ++ "description": "Gets or sets the source URL.", ++ "nullable": true ++ }, ++ "checksum": { ++ "type": "string", ++ "description": "Gets or sets a checksum for the binary.", ++ "nullable": true ++ }, ++ "timestamp": { ++ "type": "string", ++ "description": "Gets or sets a timestamp of when the binary was built.", ++ "nullable": true ++ }, ++ "repositoryName": { ++ "type": "string", ++ "description": "Gets or sets the repository name." ++ }, ++ "repositoryUrl": { ++ "type": "string", ++ "description": "Gets or sets the repository url." ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Defines the MediaBrowser.Model.Updates.VersionInfo class." ++ }, ++ "Video3DFormat": { ++ "enum": [ ++ "HalfSideBySide", ++ "FullSideBySide", ++ "FullTopAndBottom", ++ "HalfTopAndBottom", ++ "MVC" ++ ], ++ "type": "string" ++ }, ++ "VideoRange": { ++ "enum": [ ++ "Unknown", ++ "SDR", ++ "HDR" ++ ], ++ "type": "string", ++ "description": "An enum representing video ranges." ++ }, ++ "VideoRangeType": { ++ "enum": [ ++ "Unknown", ++ "SDR", ++ "HDR10", ++ "HLG", ++ "DOVI", ++ "DOVIWithHDR10", ++ "DOVIWithHLG", ++ "DOVIWithSDR", ++ "DOVIWithEL", ++ "DOVIWithHDR10Plus", ++ "DOVIWithELHDR10Plus", ++ "DOVIInvalid", ++ "HDR10Plus" ++ ], ++ "type": "string", ++ "description": "An enum representing types of video ranges." ++ }, ++ "VideoType": { ++ "enum": [ ++ "VideoFile", ++ "Iso", ++ "Dvd", ++ "BluRay" ++ ], ++ "type": "string", ++ "description": "Enum VideoType." ++ }, ++ "VirtualFolderInfo": { ++ "type": "object", ++ "properties": { ++ "Name": { ++ "type": "string", ++ "description": "Gets or sets the name.", ++ "nullable": true ++ }, ++ "Locations": { ++ "type": "array", ++ "items": { ++ "type": "string" ++ }, ++ "description": "Gets or sets the locations.", ++ "nullable": true ++ }, ++ "CollectionType": { ++ "enum": [ ++ "movies", ++ "tvshows", ++ "music", ++ "musicvideos", ++ "homevideos", ++ "boxsets", ++ "books", ++ "mixed" ++ ], ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/CollectionTypeOptions" ++ } ++ ], ++ "description": "Gets or sets the type of the collection.", ++ "nullable": true ++ }, ++ "LibraryOptions": { ++ "allOf": [ ++ { ++ "$ref": "#/components/schemas/LibraryOptions" ++ } ++ ], ++ "nullable": true ++ }, ++ "ItemId": { ++ "type": "string", ++ "description": "Gets or sets the item identifier.", ++ "nullable": true ++ }, ++ "PrimaryImageItemId": { ++ "type": "string", ++ "description": "Gets or sets the primary image item identifier.", ++ "nullable": true ++ }, ++ "RefreshProgress": { ++ "type": "number", ++ "format": "double", ++ "nullable": true ++ }, ++ "RefreshStatus": { ++ "type": "string", ++ "nullable": true ++ } ++ }, ++ "additionalProperties": false, ++ "description": "Used to hold information about a user's list of configured virtual folders." ++ }, ++ "WebSocketMessage": { ++ "type": "object", ++ "oneOf": [ ++ { ++ "$ref": "#/components/schemas/InboundWebSocketMessage" ++ }, ++ { ++ "$ref": "#/components/schemas/OutboundWebSocketMessage" ++ } ++ ], ++ "description": "Represents the possible websocket types" ++ }, ++ "XbmcMetadataOptions": { ++ "type": "object", ++ "properties": { ++ "UserId": { ++ "type": "string", ++ "nullable": true ++ }, ++ "ReleaseDateFormat": { ++ "type": "string" ++ }, ++ "SaveImagePathsInNfo": { ++ "type": "boolean" ++ }, ++ "EnablePathSubstitution": { ++ "type": "boolean" ++ }, ++ "EnableExtraThumbsDuplication": { ++ "type": "boolean" ++ } ++ }, ++ "additionalProperties": false ++ } ++ }, ++ "securitySchemes": { ++ "CustomAuthentication": { ++ "type": "apiKey", ++ "description": "API key header parameter", ++ "name": "Authorization", ++ "in": "header" ++ } ++ } ++ } ++} +\ No newline at end of file +diff --git a/docker-compose.yml b/docker-compose.yml +index f9b07e9..f937814 100644 +--- a/docker-compose.yml ++++ b/docker-compose.yml +@@ -1,25 +1,84 @@ + services: +- octo-fiestarr: +- image: ghcr.io/bransoned/octo-fiestarr +- container_name: octo-fiestarr ++ redis: ++ image: redis:7-alpine ++ container_name: allstarr-redis ++ restart: unless-stopped ++ # Redis is only accessible internally - no external port exposure ++ expose: ++ - "6379" ++ command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru ++ healthcheck: ++ test: ["CMD", "redis-cli", "ping"] ++ interval: 10s ++ timeout: 3s ++ retries: 3 ++ networks: ++ - allstarr-network ++ ++ allstarr: ++ build: ++ context: . ++ dockerfile: Dockerfile ++ image: allstarr:local ++ container_name: allstarr + restart: unless-stopped + ports: + - "5274:8080" ++ depends_on: ++ redis: ++ condition: service_healthy ++ healthcheck: ++ test: ["CMD", "curl", "-f", "http://localhost:8080/health"] ++ interval: 30s ++ timeout: 10s ++ retries: 3 ++ start_period: 40s ++ networks: ++ - allstarr-network + environment: + - ASPNETCORE_ENVIRONMENT=Production +- # Navidrome/Subsonic server URL ++ # Backend type: Subsonic or Jellyfin (default: Subsonic) ++ - Backend__Type=${BACKEND_TYPE:-Subsonic} ++ ++ # ===== REDIS CACHE ===== ++ - Redis__ConnectionString=redis:6379 ++ - Redis__Enabled=${REDIS_ENABLED:-true} ++ ++ # ===== SUBSONIC BACKEND ===== + - Subsonic__Url=${SUBSONIC_URL:-http://localhost:4533} +- # Explicit content filter: All, ExplicitOnly, CleanOnly (default: ExplicitOnly) + - Subsonic__ExplicitFilter=${EXPLICIT_FILTER:-ExplicitOnly} +- # Download mode: Track (only requested track), Album (full album when playing a track) + - Subsonic__DownloadMode=${DOWNLOAD_MODE:-Track} +- # Download path inside container ++ - Subsonic__MusicService=${MUSIC_SERVICE:-SquidWTF} ++ - Subsonic__StorageMode=${STORAGE_MODE:-Permanent} ++ - Subsonic__CacheDurationHours=${CACHE_DURATION_HOURS:-1} ++ - Subsonic__EnableExternalPlaylists=${ENABLE_EXTERNAL_PLAYLISTS:-true} ++ ++ # ===== JELLYFIN BACKEND ===== ++ - Jellyfin__Url=${JELLYFIN_URL:-http://localhost:8096} ++ - Jellyfin__ApiKey=${JELLYFIN_API_KEY:-} ++ - Jellyfin__UserId=${JELLYFIN_USER_ID:-} ++ - Jellyfin__LibraryId=${JELLYFIN_LIBRARY_ID:-} ++ - Jellyfin__ClientUsername=${JELLYFIN_CLIENT_USERNAME:-} ++ - Jellyfin__ExplicitFilter=${EXPLICIT_FILTER:-ExplicitOnly} ++ - Jellyfin__DownloadMode=${DOWNLOAD_MODE:-Track} ++ - Jellyfin__MusicService=${MUSIC_SERVICE:-SquidWTF} ++ - Jellyfin__StorageMode=${STORAGE_MODE:-Permanent} ++ - Jellyfin__CacheDurationHours=${CACHE_DURATION_HOURS:-1} ++ - Jellyfin__EnableExternalPlaylists=${ENABLE_EXTERNAL_PLAYLISTS:-true} ++ ++ # ===== SHARED ===== + - Library__DownloadPath=/app/downloads +- # SquidWTF preferred audio quality + - SquidWTF__Quality=${SQUIDWTF_QUALITY:-FLAC} +- # Set cache or permanent download of external songs +- - Storage__Mode=${STORAGE_MODE:-Permanent} +- # Set cache duration +- - Cache__DurationHours=${CACHE_DURATION_HOURS:-1} ++ - Deezer__Arl=${DEEZER_ARL:-} ++ - Deezer__ArlFallback=${DEEZER_ARL_FALLBACK:-} ++ - Deezer__Quality=${DEEZER_QUALITY:-FLAC} ++ - Qobuz__UserAuthToken=${QOBUZ_USER_AUTH_TOKEN:-} ++ - Qobuz__UserId=${QOBUZ_USER_ID:-} ++ - Qobuz__Quality=${QOBUZ_QUALITY:-FLAC} + volumes: + - ${DOWNLOAD_PATH:-./downloads}:/app/downloads ++ ++networks: ++ allstarr-network: ++ name: allstarr-network ++ driver: bridge +diff --git a/originals/octo-fiestarr/.env.example b/originals/octo-fiestarr/.env.example +new file mode 100644 +index 0000000..ff114a1 +--- /dev/null ++++ b/originals/octo-fiestarr/.env.example +@@ -0,0 +1,74 @@ ++# Navidrome/Subsonic server URL ++SUBSONIC_URL=http://localhost:4533 ++ ++# Path where downloaded songs will be stored on the host (only applies if STORAGE_MODE=Permanent) ++DOWNLOAD_PATH=./downloads ++ ++# Music service to use: SquidWTF, Deezer, or Qobuz (default: SquidWTF) ++MUSIC_SERVICE=SquidWTF ++ ++# ===== SquidWTF CONFIGURATION ===== ++# Different quality options for SquidWTF. Only FLAC supported right now ++SQUIDWTF_QUALITY=FLAC ++ ++# ===== DEEZER CONFIGURATION ===== ++# Deezer ARL token (required if using Deezer) ++# See README.md for instructions on how to get this token ++DEEZER_ARL=your-deezer-arl-token ++ ++# Fallback ARL token (optional) ++DEEZER_ARL_FALLBACK= ++ ++# Preferred audio quality: FLAC, MP3_320, MP3_128 (optional) ++# If not specified, the highest available quality for your account will be used ++DEEZER_QUALITY= ++ ++# ===== QOBUZ CONFIGURATION ===== ++# Qobuz user authentication token (required if using Qobuz) ++# Get this from your browser after logging into play.qobuz.com ++# See README.md for detailed instructions ++QOBUZ_USER_AUTH_TOKEN= ++ ++# Qobuz user ID (required if using Qobuz) ++# Get this from your browser after logging into play.qobuz.com ++QOBUZ_USER_ID= ++ ++# Preferred audio quality: FLAC, FLAC_24_HIGH, FLAC_24_LOW, FLAC_16, MP3_320 (optional) ++# If not specified, the highest available quality will be used ++QOBUZ_QUALITY= ++ ++# ===== GENERAL SETTINGS ===== ++# External playlists support (optional, default: true) ++# When enabled, allows searching and downloading playlists from Deezer/Qobuz ++# Starring a playlist triggers automatic download of all tracks and creates an M3U file ++ENABLE_EXTERNAL_PLAYLISTS=true ++ ++# Playlists directory name (optional, default: playlists) ++# M3U playlist files will be created in {DOWNLOAD_PATH}/{PLAYLISTS_DIRECTORY}/ ++PLAYLISTS_DIRECTORY=playlists ++ ++# Explicit content filter (optional, default: All) ++# - All: Show all tracks (no filtering) ++# - ExplicitOnly: Exclude clean/edited versions, keep original explicit content ++# - CleanOnly: Only show clean content (naturally clean or edited versions) ++# Note: This only works with Deezer, Qobuz doesn't expose explicit content flags ++EXPLICIT_FILTER=All ++ ++# Download mode (optional, default: Track) ++# - Track: Download only the played track ++# - Album: When playing a track, download the entire album in background ++# The played track is downloaded first, remaining tracks are queued ++DOWNLOAD_MODE=Track ++ ++# Storage mode (optional, default: Permanent) ++# - Permanent: Files are saved to the library permanently and registered in Navidrome ++# - Cache: Files are stored in /tmp and automatically cleaned up after CACHE_DURATION_HOURS ++# Not registered in Navidrome, ideal for streaming without library bloat ++# Note: On Linux/Docker, you can customize cache location by setting TMPDIR environment variable ++STORAGE_MODE=Permanent ++ ++# Cache duration in hours (optional, default: 1) ++# Files older than this duration will be automatically deleted when STORAGE_MODE=Cache ++# Based on last access time (updated each time the file is streamed) ++# Cache location: /tmp/octo-fiesta-cache (or $TMPDIR/octo-fiesta-cache if TMPDIR is set) ++CACHE_DURATION_HOURS=1 +diff --git a/originals/octo-fiestarr/.github/workflows/ci.yml b/originals/octo-fiestarr/.github/workflows/ci.yml +new file mode 100644 +index 0000000..685235d +--- /dev/null ++++ b/originals/octo-fiestarr/.github/workflows/ci.yml +@@ -0,0 +1,33 @@ ++name: CI ++ ++on: ++ push: ++ branches: [master, dev] ++ pull_request: ++ types: [opened, synchronize, reopened] ++ branches: [master, dev] ++ ++env: ++ DOTNET_VERSION: "9.0.x" ++ ++jobs: ++ build-and-test: ++ runs-on: ubuntu-latest ++ ++ steps: ++ - name: Checkout ++ uses: actions/checkout@v4 ++ ++ - name: Setup .NET ++ uses: actions/setup-dotnet@v4 ++ with: ++ dotnet-version: ${{ env.DOTNET_VERSION }} ++ ++ - name: Restore dependencies ++ run: dotnet restore ++ ++ - name: Build ++ run: dotnet build --configuration Release --no-restore ++ ++ - name: Test ++ run: dotnet test --configuration Release --no-build --verbosity normal +diff --git a/originals/octo-fiestarr/.github/workflows/docker.yml b/originals/octo-fiestarr/.github/workflows/docker.yml +new file mode 100644 +index 0000000..36c852b +--- /dev/null ++++ b/originals/octo-fiestarr/.github/workflows/docker.yml +@@ -0,0 +1,91 @@ ++name: Docker Build & Push ++ ++on: ++ workflow_dispatch: ++ push: ++ tags: ['v*'] ++ branches: [master, dev] ++ pull_request: ++ types: [closed] ++ branches: [master, dev] ++ ++env: ++ DOTNET_VERSION: "9.0.x" ++ REGISTRY: ghcr.io ++ IMAGE_NAME: ${{ github.repository }} ++ ++jobs: ++ build-and-test: ++ runs-on: ubuntu-latest ++ ++ steps: ++ - name: Checkout ++ uses: actions/checkout@v4 ++ ++ - name: Setup .NET ++ uses: actions/setup-dotnet@v4 ++ with: ++ dotnet-version: ${{ env.DOTNET_VERSION }} ++ ++ - name: Restore dependencies ++ run: dotnet restore ++ ++ - name: Build ++ run: dotnet build --configuration Release --no-restore ++ ++ - name: Test ++ run: dotnet test --configuration Release --no-build --verbosity normal ++ ++ docker: ++ needs: build-and-test ++ runs-on: ubuntu-latest ++ # Only run docker build/push on merged PRs, tags, or manual triggers ++ if: | ++ github.event_name == 'workflow_dispatch' || ++ github.event_name == 'push' || ++ (github.event_name == 'pull_request' && github.event.pull_request.merged == true) ++ ++ permissions: ++ contents: read ++ packages: write ++ ++ steps: ++ - name: Checkout ++ uses: actions/checkout@v4 ++ ++ - name: Set up QEMU ++ uses: docker/setup-qemu-action@v3 ++ ++ - name: Set up Docker Buildx ++ uses: docker/setup-buildx-action@v3 ++ ++ - name: Login to GitHub Container Registry ++ uses: docker/login-action@v3 ++ with: ++ registry: ${{ env.REGISTRY }} ++ username: ${{ github.actor }} ++ password: ${{ secrets.GITHUB_TOKEN }} ++ ++ - name: Extract metadata ++ id: meta ++ uses: docker/metadata-action@v5 ++ with: ++ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} ++ tags: | ++ type=ref,event=branch ++ type=sha,prefix= ++ type=semver,pattern={{version}} ++ type=semver,pattern={{major}}.{{minor}} ++ type=semver,pattern={{major}} ++ type=raw,value=latest,enable=${{ github.ref == 'refs/heads/dev' || github.event_name == 'workflow_dispatch' }} ++ ++ - name: Build and push ++ uses: docker/build-push-action@v6 ++ with: ++ context: . ++ platforms: linux/amd64,linux/arm64 ++ push: true ++ tags: ${{ steps.meta.outputs.tags }} ++ labels: ${{ steps.meta.outputs.labels }} ++ cache-from: type=gha ++ cache-to: type=gha,mode=max +diff --git a/originals/octo-fiestarr/.gitignore b/originals/octo-fiestarr/.gitignore +new file mode 100644 +index 0000000..81b563b +--- /dev/null ++++ b/originals/octo-fiestarr/.gitignore +@@ -0,0 +1,74 @@ ++## A streamlined .gitignore for modern .NET projects ++## including temporary files, build results, and ++## files generated by popular .NET tools. If you are ++## developing with Visual Studio, the VS .gitignore ++## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore ++## has more thorough IDE-specific entries. ++## ++## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore ++ ++# Build results ++[Dd]ebug/ ++[Dd]ebugPublic/ ++[Rr]elease/ ++[Rr]eleases/ ++x64/ ++x86/ ++[Ww][Ii][Nn]32/ ++[Aa][Rr][Mm]/ ++[Aa][Rr][Mm]64/ ++bld/ ++[Bb]in/ ++[Oo]bj/ ++[Ll]og/ ++[Ll]ogs/ ++ ++# .NET Core ++project.lock.json ++project.fragment.lock.json ++artifacts/ ++ ++# ASP.NET Scaffolding ++ScaffoldingReadMe.txt ++ ++# NuGet Packages ++*.nupkg ++# NuGet Symbol Packages ++*.snupkg ++ ++# Others ++~$* ++*~ ++CodeCoverage/ ++ ++# MSBuild Binary and Structured Log ++*.binlog ++ ++# MSTest test Results ++[Tt]est[Rr]esult*/ ++[Bb]uild[Ll]og.* ++ ++# NUnit ++*.VisualState.xml ++TestResult.xml ++nunit-*.xml ++ ++# Fichiers et dossiers à ignorer pour un projet .NET ++bin/ ++obj/ ++*.user ++*.suo ++*.userosscache ++*.sln.docstates ++*.vs/ ++# Rider ++.idea/ ++# Visual Studio Code ++.vscode/ ++# Autres fichiers temporaires ++*.log ++ ++/.env ++ ++# Downloaded music files ++octo-fiesta/downloads/ +\ No newline at end of file +diff --git a/originals/octo-fiestarr/Dockerfile b/originals/octo-fiestarr/Dockerfile +new file mode 100644 +index 0000000..f49119c +--- /dev/null ++++ b/originals/octo-fiestarr/Dockerfile +@@ -0,0 +1,27 @@ ++# Build stage ++FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ++WORKDIR /src ++ ++COPY octo-fiesta.sln . ++COPY octo-fiesta/octo-fiesta.csproj octo-fiesta/ ++COPY octo-fiesta.Tests/octo-fiesta.Tests.csproj octo-fiesta.Tests/ ++ ++RUN dotnet restore ++ ++COPY octo-fiesta/ octo-fiesta/ ++COPY octo-fiesta.Tests/ octo-fiesta.Tests/ ++ ++RUN dotnet publish octo-fiesta/octo-fiesta.csproj -c Release -o /app/publish ++ ++# Runtime stage ++FROM mcr.microsoft.com/dotnet/aspnet:9.0 ++WORKDIR /app ++ ++RUN mkdir -p /app/downloads ++ ++COPY --from=build /app/publish . ++ ++EXPOSE 8080 ++ENV ASPNETCORE_URLS=http://+:8080 ++ ++ENTRYPOINT ["dotnet", "octo-fiesta.dll"] +diff --git a/originals/octo-fiestarr/LICENSE b/originals/octo-fiestarr/LICENSE +new file mode 100644 +index 0000000..f288702 +--- /dev/null ++++ b/originals/octo-fiestarr/LICENSE +@@ -0,0 +1,674 @@ ++ GNU GENERAL PUBLIC LICENSE ++ Version 3, 29 June 2007 ++ ++ Copyright (C) 2007 Free Software Foundation, Inc. ++ Everyone is permitted to copy and distribute verbatim copies ++ of this license document, but changing it is not allowed. ++ ++ Preamble ++ ++ The GNU General Public License is a free, copyleft license for ++software and other kinds of works. ++ ++ The licenses for most software and other practical works are designed ++to take away your freedom to share and change the works. By contrast, ++the GNU General Public License is intended to guarantee your freedom to ++share and change all versions of a program--to make sure it remains free ++software for all its users. We, the Free Software Foundation, use the ++GNU General Public License for most of our software; it applies also to ++any other work released this way by its authors. You can apply it to ++your programs, too. ++ ++ When we speak of free software, we are referring to freedom, not ++price. Our General Public Licenses are designed to make sure that you ++have the freedom to distribute copies of free software (and charge for ++them if you wish), that you receive source code or can get it if you ++want it, that you can change the software or use pieces of it in new ++free programs, and that you know you can do these things. ++ ++ To protect your rights, we need to prevent others from denying you ++these rights or asking you to surrender the rights. Therefore, you have ++certain responsibilities if you distribute copies of the software, or if ++you modify it: responsibilities to respect the freedom of others. ++ ++ For example, if you distribute copies of such a program, whether ++gratis or for a fee, you must pass on to the recipients the same ++freedoms that you received. You must make sure that they, too, receive ++or can get the source code. And you must show them these terms so they ++know their rights. ++ ++ Developers that use the GNU GPL protect your rights with two steps: ++(1) assert copyright on the software, and (2) offer you this License ++giving you legal permission to copy, distribute and/or modify it. ++ ++ For the developers' and authors' protection, the GPL clearly explains ++that there is no warranty for this free software. For both users' and ++authors' sake, the GPL requires that modified versions be marked as ++changed, so that their problems will not be attributed erroneously to ++authors of previous versions. ++ ++ Some devices are designed to deny users access to install or run ++modified versions of the software inside them, although the manufacturer ++can do so. This is fundamentally incompatible with the aim of ++protecting users' freedom to change the software. The systematic ++pattern of such abuse occurs in the area of products for individuals to ++use, which is precisely where it is most unacceptable. Therefore, we ++have designed this version of the GPL to prohibit the practice for those ++products. If such problems arise substantially in other domains, we ++stand ready to extend this provision to those domains in future versions ++of the GPL, as needed to protect the freedom of users. ++ ++ Finally, every program is threatened constantly by software patents. ++States should not allow patents to restrict development and use of ++software on general-purpose computers, but in those that do, we wish to ++avoid the special danger that patents applied to a free program could ++make it effectively proprietary. To prevent this, the GPL assures that ++patents cannot be used to render the program non-free. ++ ++ The precise terms and conditions for copying, distribution and ++modification follow. ++ ++ TERMS AND CONDITIONS ++ ++ 0. Definitions. ++ ++ "This License" refers to version 3 of the GNU General Public License. ++ ++ "Copyright" also means copyright-like laws that apply to other kinds of ++works, such as semiconductor masks. ++ ++ "The Program" refers to any copyrightable work licensed under this ++License. Each licensee is addressed as "you". "Licensees" and ++"recipients" may be individuals or organizations. ++ ++ To "modify" a work means to copy from or adapt all or part of the work ++in a fashion requiring copyright permission, other than the making of an ++exact copy. The resulting work is called a "modified version" of the ++earlier work or a work "based on" the earlier work. ++ ++ A "covered work" means either the unmodified Program or a work based ++on the Program. ++ ++ To "propagate" a work means to do anything with it that, without ++permission, would make you directly or secondarily liable for ++infringement under applicable copyright law, except executing it on a ++computer or modifying a private copy. Propagation includes copying, ++distribution (with or without modification), making available to the ++public, and in some countries other activities as well. ++ ++ To "convey" a work means any kind of propagation that enables other ++parties to make or receive copies. Mere interaction with a user through ++a computer network, with no transfer of a copy, is not conveying. ++ ++ An interactive user interface displays "Appropriate Legal Notices" ++to the extent that it includes a convenient and prominently visible ++feature that (1) displays an appropriate copyright notice, and (2) ++tells the user that there is no warranty for the work (except to the ++extent that warranties are provided), that licensees may convey the ++work under this License, and how to view a copy of this License. If ++the interface presents a list of user commands or options, such as a ++menu, a prominent item in the list meets this criterion. ++ ++ 1. Source Code. ++ ++ The "source code" for a work means the preferred form of the work ++for making modifications to it. "Object code" means any non-source ++form of a work. ++ ++ A "Standard Interface" means an interface that either is an official ++standard defined by a recognized standards body, or, in the case of ++interfaces specified for a particular programming language, one that ++is widely used among developers working in that language. ++ ++ The "System Libraries" of an executable work include anything, other ++than the work as a whole, that (a) is included in the normal form of ++packaging a Major Component, but which is not part of that Major ++Component, and (b) serves only to enable use of the work with that ++Major Component, or to implement a Standard Interface for which an ++implementation is available to the public in source code form. A ++"Major Component", in this context, means a major essential component ++(kernel, window system, and so on) of the specific operating system ++(if any) on which the executable work runs, or a compiler used to ++produce the work, or an object code interpreter used to run it. ++ ++ The "Corresponding Source" for a work in object code form means all ++the source code needed to generate, install, and (for an executable ++work) run the object code and to modify the work, including scripts to ++control those activities. However, it does not include the work's ++System Libraries, or general-purpose tools or generally available free ++programs which are used unmodified in performing those activities but ++which are not part of the work. For example, Corresponding Source ++includes interface definition files associated with source files for ++the work, and the source code for shared libraries and dynamically ++linked subprograms that the work is specifically designed to require, ++such as by intimate data communication or control flow between those ++subprograms and other parts of the work. ++ ++ The Corresponding Source need not include anything that users ++can regenerate automatically from other parts of the Corresponding ++Source. ++ ++ The Corresponding Source for a work in source code form is that ++same work. ++ ++ 2. Basic Permissions. ++ ++ All rights granted under this License are granted for the term of ++copyright on the Program, and are irrevocable provided the stated ++conditions are met. This License explicitly affirms your unlimited ++permission to run the unmodified Program. The output from running a ++covered work is covered by this License only if the output, given its ++content, constitutes a covered work. This License acknowledges your ++rights of fair use or other equivalent, as provided by copyright law. ++ ++ You may make, run and propagate covered works that you do not ++convey, without conditions so long as your license otherwise remains ++in force. You may convey covered works to others for the sole purpose ++of having them make modifications exclusively for you, or provide you ++with facilities for running those works, provided that you comply with ++the terms of this License in conveying all material for which you do ++not control copyright. Those thus making or running the covered works ++for you must do so exclusively on your behalf, under your direction ++and control, on terms that prohibit them from making any copies of ++your copyrighted material outside their relationship with you. ++ ++ Conveying under any other circumstances is permitted solely under ++the conditions stated below. Sublicensing is not allowed; section 10 ++makes it unnecessary. ++ ++ 3. Protecting Users' Legal Rights From Anti-Circumvention Law. ++ ++ No covered work shall be deemed part of an effective technological ++measure under any applicable law fulfilling obligations under article ++11 of the WIPO copyright treaty adopted on 20 December 1996, or ++similar laws prohibiting or restricting circumvention of such ++measures. ++ ++ When you convey a covered work, you waive any legal power to forbid ++circumvention of technological measures to the extent such circumvention ++is effected by exercising rights under this License with respect to ++the covered work, and you disclaim any intention to limit operation or ++modification of the work as a means of enforcing, against the work's ++users, your or third parties' legal rights to forbid circumvention of ++technological measures. ++ ++ 4. Conveying Verbatim Copies. ++ ++ You may convey verbatim copies of the Program's source code as you ++receive it, in any medium, provided that you conspicuously and ++appropriately publish on each copy an appropriate copyright notice; ++keep intact all notices stating that this License and any ++non-permissive terms added in accord with section 7 apply to the code; ++keep intact all notices of the absence of any warranty; and give all ++recipients a copy of this License along with the Program. ++ ++ You may charge any price or no price for each copy that you convey, ++and you may offer support or warranty protection for a fee. ++ ++ 5. Conveying Modified Source Versions. ++ ++ You may convey a work based on the Program, or the modifications to ++produce it from the Program, in the form of source code under the ++terms of section 4, provided that you also meet all of these conditions: ++ ++ a) The work must carry prominent notices stating that you modified ++ it, and giving a relevant date. ++ ++ b) The work must carry prominent notices stating that it is ++ released under this License and any conditions added under section ++ 7. This requirement modifies the requirement in section 4 to ++ "keep intact all notices". ++ ++ c) You must license the entire work, as a whole, under this ++ License to anyone who comes into possession of a copy. This ++ License will therefore apply, along with any applicable section 7 ++ additional terms, to the whole of the work, and all its parts, ++ regardless of how they are packaged. This License gives no ++ permission to license the work in any other way, but it does not ++ invalidate such permission if you have separately received it. ++ ++ d) If the work has interactive user interfaces, each must display ++ Appropriate Legal Notices; however, if the Program has interactive ++ interfaces that do not display Appropriate Legal Notices, your ++ work need not make them do so. ++ ++ A compilation of a covered work with other separate and independent ++works, which are not by their nature extensions of the covered work, ++and which are not combined with it such as to form a larger program, ++in or on a volume of a storage or distribution medium, is called an ++"aggregate" if the compilation and its resulting copyright are not ++used to limit the access or legal rights of the compilation's users ++beyond what the individual works permit. Inclusion of a covered work ++in an aggregate does not cause this License to apply to the other ++parts of the aggregate. ++ ++ 6. Conveying Non-Source Forms. ++ ++ You may convey a covered work in object code form under the terms ++of sections 4 and 5, provided that you also convey the ++machine-readable Corresponding Source under the terms of this License, ++in one of these ways: ++ ++ a) Convey the object code in, or embodied in, a physical product ++ (including a physical distribution medium), accompanied by the ++ Corresponding Source fixed on a durable physical medium ++ customarily used for software interchange. ++ ++ b) Convey the object code in, or embodied in, a physical product ++ (including a physical distribution medium), accompanied by a ++ written offer, valid for at least three years and valid for as ++ long as you offer spare parts or customer support for that product ++ model, to give anyone who possesses the object code either (1) a ++ copy of the Corresponding Source for all the software in the ++ product that is covered by this License, on a durable physical ++ medium customarily used for software interchange, for a price no ++ more than your reasonable cost of physically performing this ++ conveying of source, or (2) access to copy the ++ Corresponding Source from a network server at no charge. ++ ++ c) Convey individual copies of the object code with a copy of the ++ written offer to provide the Corresponding Source. This ++ alternative is allowed only occasionally and noncommercially, and ++ only if you received the object code with such an offer, in accord ++ with subsection 6b. ++ ++ d) Convey the object code by offering access from a designated ++ place (gratis or for a charge), and offer equivalent access to the ++ Corresponding Source in the same way through the same place at no ++ further charge. You need not require recipients to copy the ++ Corresponding Source along with the object code. If the place to ++ copy the object code is a network server, the Corresponding Source ++ may be on a different server (operated by you or a third party) ++ that supports equivalent copying facilities, provided you maintain ++ clear directions next to the object code saying where to find the ++ Corresponding Source. Regardless of what server hosts the ++ Corresponding Source, you remain obligated to ensure that it is ++ available for as long as needed to satisfy these requirements. ++ ++ e) Convey the object code using peer-to-peer transmission, provided ++ you inform other peers where the object code and Corresponding ++ Source of the work are being offered to the general public at no ++ charge under subsection 6d. ++ ++ A separable portion of the object code, whose source code is excluded ++from the Corresponding Source as a System Library, need not be ++included in conveying the object code work. ++ ++ A "User Product" is either (1) a "consumer product", which means any ++tangible personal property which is normally used for personal, family, ++or household purposes, or (2) anything designed or sold for incorporation ++into a dwelling. In determining whether a product is a consumer product, ++doubtful cases shall be resolved in favor of coverage. For a particular ++product received by a particular user, "normally used" refers to a ++typical or common use of that class of product, regardless of the status ++of the particular user or of the way in which the particular user ++actually uses, or expects or is expected to use, the product. A product ++is a consumer product regardless of whether the product has substantial ++commercial, industrial or non-consumer uses, unless such uses represent ++the only significant mode of use of the product. ++ ++ "Installation Information" for a User Product means any methods, ++procedures, authorization keys, or other information required to install ++and execute modified versions of a covered work in that User Product from ++a modified version of its Corresponding Source. The information must ++suffice to ensure that the continued functioning of the modified object ++code is in no case prevented or interfered with solely because ++modification has been made. ++ ++ If you convey an object code work under this section in, or with, or ++specifically for use in, a User Product, and the conveying occurs as ++part of a transaction in which the right of possession and use of the ++User Product is transferred to the recipient in perpetuity or for a ++fixed term (regardless of how the transaction is characterized), the ++Corresponding Source conveyed under this section must be accompanied ++by the Installation Information. But this requirement does not apply ++if neither you nor any third party retains the ability to install ++modified object code on the User Product (for example, the work has ++been installed in ROM). ++ ++ The requirement to provide Installation Information does not include a ++requirement to continue to provide support service, warranty, or updates ++for a work that has been modified or installed by the recipient, or for ++the User Product in which it has been modified or installed. Access to a ++network may be denied when the modification itself materially and ++adversely affects the operation of the network or violates the rules and ++protocols for communication across the network. ++ ++ Corresponding Source conveyed, and Installation Information provided, ++in accord with this section must be in a format that is publicly ++documented (and with an implementation available to the public in ++source code form), and must require no special password or key for ++unpacking, reading or copying. ++ ++ 7. Additional Terms. ++ ++ "Additional permissions" are terms that supplement the terms of this ++License by making exceptions from one or more of its conditions. ++Additional permissions that are applicable to the entire Program shall ++be treated as though they were included in this License, to the extent ++that they are valid under applicable law. If additional permissions ++apply only to part of the Program, that part may be used separately ++under those permissions, but the entire Program remains governed by ++this License without regard to the additional permissions. ++ ++ When you convey a copy of a covered work, you may at your option ++remove any additional permissions from that copy, or from any part of ++it. (Additional permissions may be written to require their own ++removal in certain cases when you modify the work.) You may place ++additional permissions on material, added by you to a covered work, ++for which you have or can give appropriate copyright permission. ++ ++ Notwithstanding any other provision of this License, for material you ++add to a covered work, you may (if authorized by the copyright holders of ++that material) supplement the terms of this License with terms: ++ ++ a) Disclaiming warranty or limiting liability differently from the ++ terms of sections 15 and 16 of this License; or ++ ++ b) Requiring preservation of specified reasonable legal notices or ++ author attributions in that material or in the Appropriate Legal ++ Notices displayed by works containing it; or ++ ++ c) Prohibiting misrepresentation of the origin of that material, or ++ requiring that modified versions of such material be marked in ++ reasonable ways as different from the original version; or ++ ++ d) Limiting the use for publicity purposes of names of licensors or ++ authors of the material; or ++ ++ e) Declining to grant rights under trademark law for use of some ++ trade names, trademarks, or service marks; or ++ ++ f) Requiring indemnification of licensors and authors of that ++ material by anyone who conveys the material (or modified versions of ++ it) with contractual assumptions of liability to the recipient, for ++ any liability that these contractual assumptions directly impose on ++ those licensors and authors. ++ ++ All other non-permissive additional terms are considered "further ++restrictions" within the meaning of section 10. If the Program as you ++received it, or any part of it, contains a notice stating that it is ++governed by this License along with a term that is a further ++restriction, you may remove that term. If a license document contains ++a further restriction but permits relicensing or conveying under this ++License, you may add to a covered work material governed by the terms ++of that license document, provided that the further restriction does ++not survive such relicensing or conveying. ++ ++ If you add terms to a covered work in accord with this section, you ++must place, in the relevant source files, a statement of the ++additional terms that apply to those files, or a notice indicating ++where to find the applicable terms. ++ ++ Additional terms, permissive or non-permissive, may be stated in the ++form of a separately written license, or stated as exceptions; ++the above requirements apply either way. ++ ++ 8. Termination. ++ ++ You may not propagate or modify a covered work except as expressly ++provided under this License. Any attempt otherwise to propagate or ++modify it is void, and will automatically terminate your rights under ++this License (including any patent licenses granted under the third ++paragraph of section 11). ++ ++ However, if you cease all violation of this License, then your ++license from a particular copyright holder is reinstated (a) ++provisionally, unless and until the copyright holder explicitly and ++finally terminates your license, and (b) permanently, if the copyright ++holder fails to notify you of the violation by some reasonable means ++prior to 60 days after the cessation. ++ ++ Moreover, your license from a particular copyright holder is ++reinstated permanently if the copyright holder notifies you of the ++violation by some reasonable means, this is the first time you have ++received notice of violation of this License (for any work) from that ++copyright holder, and you cure the violation prior to 30 days after ++your receipt of the notice. ++ ++ Termination of your rights under this section does not terminate the ++licenses of parties who have received copies or rights from you under ++this License. If your rights have been terminated and not permanently ++reinstated, you do not qualify to receive new licenses for the same ++material under section 10. ++ ++ 9. Acceptance Not Required for Having Copies. ++ ++ You are not required to accept this License in order to receive or ++run a copy of the Program. Ancillary propagation of a covered work ++occurring solely as a consequence of using peer-to-peer transmission ++to receive a copy likewise does not require acceptance. However, ++nothing other than this License grants you permission to propagate or ++modify any covered work. These actions infringe copyright if you do ++not accept this License. Therefore, by modifying or propagating a ++covered work, you indicate your acceptance of this License to do so. ++ ++ 10. Automatic Licensing of Downstream Recipients. ++ ++ Each time you convey a covered work, the recipient automatically ++receives a license from the original licensors, to run, modify and ++propagate that work, subject to this License. You are not responsible ++for enforcing compliance by third parties with this License. ++ ++ An "entity transaction" is a transaction transferring control of an ++organization, or substantially all assets of one, or subdividing an ++organization, or merging organizations. If propagation of a covered ++work results from an entity transaction, each party to that ++transaction who receives a copy of the work also receives whatever ++licenses to the work the party's predecessor in interest had or could ++give under the previous paragraph, plus a right to possession of the ++Corresponding Source of the work from the predecessor in interest, if ++the predecessor has it or can get it with reasonable efforts. ++ ++ You may not impose any further restrictions on the exercise of the ++rights granted or affirmed under this License. For example, you may ++not impose a license fee, royalty, or other charge for exercise of ++rights granted under this License, and you may not initiate litigation ++(including a cross-claim or counterclaim in a lawsuit) alleging that ++any patent claim is infringed by making, using, selling, offering for ++sale, or importing the Program or any portion of it. ++ ++ 11. Patents. ++ ++ A "contributor" is a copyright holder who authorizes use under this ++License of the Program or a work on which the Program is based. The ++work thus licensed is called the contributor's "contributor version". ++ ++ A contributor's "essential patent claims" are all patent claims ++owned or controlled by the contributor, whether already acquired or ++hereafter acquired, that would be infringed by some manner, permitted ++by this License, of making, using, or selling its contributor version, ++but do not include claims that would be infringed only as a ++consequence of further modification of the contributor version. For ++purposes of this definition, "control" includes the right to grant ++patent sublicenses in a manner consistent with the requirements of ++this License. ++ ++ Each contributor grants you a non-exclusive, worldwide, royalty-free ++patent license under the contributor's essential patent claims, to ++make, use, sell, offer for sale, import and otherwise run, modify and ++propagate the contents of its contributor version. ++ ++ In the following three paragraphs, a "patent license" is any express ++agreement or commitment, however denominated, not to enforce a patent ++(such as an express permission to practice a patent or covenant not to ++sue for patent infringement). To "grant" such a patent license to a ++party means to make such an agreement or commitment not to enforce a ++patent against the party. ++ ++ If you convey a covered work, knowingly relying on a patent license, ++and the Corresponding Source of the work is not available for anyone ++to copy, free of charge and under the terms of this License, through a ++publicly available network server or other readily accessible means, ++then you must either (1) cause the Corresponding Source to be so ++available, or (2) arrange to deprive yourself of the benefit of the ++patent license for this particular work, or (3) arrange, in a manner ++consistent with the requirements of this License, to extend the patent ++license to downstream recipients. "Knowingly relying" means you have ++actual knowledge that, but for the patent license, your conveying the ++covered work in a country, or your recipient's use of the covered work ++in a country, would infringe one or more identifiable patents in that ++country that you have reason to believe are valid. ++ ++ If, pursuant to or in connection with a single transaction or ++arrangement, you convey, or propagate by procuring conveyance of, a ++covered work, and grant a patent license to some of the parties ++receiving the covered work authorizing them to use, propagate, modify ++or convey a specific copy of the covered work, then the patent license ++you grant is automatically extended to all recipients of the covered ++work and works based on it. ++ ++ A patent license is "discriminatory" if it does not include within ++the scope of its coverage, prohibits the exercise of, or is ++conditioned on the non-exercise of one or more of the rights that are ++specifically granted under this License. You may not convey a covered ++work if you are a party to an arrangement with a third party that is ++in the business of distributing software, under which you make payment ++to the third party based on the extent of your activity of conveying ++the work, and under which the third party grants, to any of the ++parties who would receive the covered work from you, a discriminatory ++patent license (a) in connection with copies of the covered work ++conveyed by you (or copies made from those copies), or (b) primarily ++for and in connection with specific products or compilations that ++contain the covered work, unless you entered into that arrangement, ++or that patent license was granted, prior to 28 March 2007. ++ ++ Nothing in this License shall be construed as excluding or limiting ++any implied license or other defenses to infringement that may ++otherwise be available to you under applicable patent law. ++ ++ 12. No Surrender of Others' Freedom. ++ ++ If conditions are imposed on you (whether by court order, agreement or ++otherwise) that contradict the conditions of this License, they do not ++excuse you from the conditions of this License. If you cannot convey a ++covered work so as to satisfy simultaneously your obligations under this ++License and any other pertinent obligations, then as a consequence you may ++not convey it at all. For example, if you agree to terms that obligate you ++to collect a royalty for further conveying from those to whom you convey ++the Program, the only way you could satisfy both those terms and this ++License would be to refrain entirely from conveying the Program. ++ ++ 13. Use with the GNU Affero General Public License. ++ ++ Notwithstanding any other provision of this License, you have ++permission to link or combine any covered work with a work licensed ++under version 3 of the GNU Affero General Public License into a single ++combined work, and to convey the resulting work. The terms of this ++License will continue to apply to the part which is the covered work, ++but the special requirements of the GNU Affero General Public License, ++section 13, concerning interaction through a network will apply to the ++combination as such. ++ ++ 14. Revised Versions of this License. ++ ++ The Free Software Foundation may publish revised and/or new versions of ++the GNU General Public License from time to time. Such new versions will ++be similar in spirit to the present version, but may differ in detail to ++address new problems or concerns. ++ ++ Each version is given a distinguishing version number. If the ++Program specifies that a certain numbered version of the GNU General ++Public License "or any later version" applies to it, you have the ++option of following the terms and conditions either of that numbered ++version or of any later version published by the Free Software ++Foundation. If the Program does not specify a version number of the ++GNU General Public License, you may choose any version ever published ++by the Free Software Foundation. ++ ++ If the Program specifies that a proxy can decide which future ++versions of the GNU General Public License can be used, that proxy's ++public statement of acceptance of a version permanently authorizes you ++to choose that version for the Program. ++ ++ Later license versions may give you additional or different ++permissions. However, no additional obligations are imposed on any ++author or copyright holder as a result of your choosing to follow a ++later version. ++ ++ 15. Disclaimer of Warranty. ++ ++ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY ++APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT ++HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY ++OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, ++THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM ++IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ++ALL NECESSARY SERVICING, REPAIR OR CORRECTION. ++ ++ 16. Limitation of Liability. ++ ++ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING ++WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS ++THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY ++GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE ++USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF ++DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD ++PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), ++EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF ++SUCH DAMAGES. ++ ++ 17. Interpretation of Sections 15 and 16. ++ ++ If the disclaimer of warranty and limitation of liability provided ++above cannot be given local legal effect according to their terms, ++reviewing courts shall apply local law that most closely approximates ++an absolute waiver of all civil liability in connection with the ++Program, unless a warranty or assumption of liability accompanies a ++copy of the Program in return for a fee. ++ ++ END OF TERMS AND CONDITIONS ++ ++ How to Apply These Terms to Your New Programs ++ ++ If you develop a new program, and you want it to be of the greatest ++possible use to the public, the best way to achieve this is to make it ++free software which everyone can redistribute and change under these terms. ++ ++ To do so, attach the following notices to the program. It is safest ++to attach them to the start of each source file to most effectively ++state the exclusion of warranty; and each file should have at least ++the "copyright" line and a pointer to where the full notice is found. ++ ++ ++ Copyright (C) ++ ++ This program is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ This program is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with this program. If not, see . ++ ++Also add information on how to contact you by electronic and paper mail. ++ ++ If the program does terminal interaction, make it output a short ++notice like this when it starts in an interactive mode: ++ ++ Copyright (C) ++ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. ++ This is free software, and you are welcome to redistribute it ++ under certain conditions; type `show c' for details. ++ ++The hypothetical commands `show w' and `show c' should show the appropriate ++parts of the General Public License. Of course, your program's commands ++might be different; for a GUI interface, you would use an "about box". ++ ++ You should also get your employer (if you work as a programmer) or school, ++if any, to sign a "copyright disclaimer" for the program, if necessary. ++For more information on this, and how to apply and follow the GNU GPL, see ++. ++ ++ The GNU General Public License does not permit incorporating your program ++into proprietary programs. If your program is a subroutine library, you ++may consider it more useful to permit linking proprietary applications with ++the library. If this is what you want to do, use the GNU Lesser General ++Public License instead of this License. But first, please read ++. +diff --git a/originals/octo-fiestarr/README.md b/originals/octo-fiestarr/README.md +new file mode 100644 +index 0000000..93dce47 +--- /dev/null ++++ b/originals/octo-fiestarr/README.md +@@ -0,0 +1,476 @@ ++# Octo-Fiestarr ++ ++A Subsonic API proxy server that transparently integrates multiple music streaming providers as sources. When a song is not available in your local Navidrome library, it is automatically fetched from your configured provider, downloaded, and served to your Subsonic-compatible client. The downloaded song is then added to your library, making it available locally for future listens. ++ ++## Why "Octo-Fiestarr"? ++ ++This fork was created to focus on integrating the original concept of Octo-Fiesta with music providers that do not require API credentials, such as SquidWTF. This allows for seamless external music discovery without the need for any subscriptions. Thus, I saw it fitting to change the name of the fork to resemble other *arr projects. ++ ++## Features ++ ++- **Multi-Provider Architecture**: Pluggable music service system supporting multiple streaming providers (Deezer, Qobuz, and more to come) ++- **Transparent Proxy**: Acts as a middleware between Subsonic clients (like Aonsoku, Sublime Music, etc.) and your Navidrome server ++- **Seamless Integration**: Automatically searches and streams music from your configured provider when not available locally ++- **Automatic Downloads**: Songs are downloaded on-the-fly and cached for future use ++- **External Playlist Support**: Search, discover, and download playlists from Deezer, Qobuz, and SquidWTF with automatic M3U generation ++- **Hi-Res Audio Support**: SquidWTF provider supports up to 24-bit/192kHz FLAC quality ++- **Full Metadata Embedding**: Downloaded files include complete ID3 tags (title, artist, album, track number, year, genre, BPM, ISRC, etc.) and embedded cover art ++- **Organized Library**: Downloads are saved in a clean `Artist/Album/Track` folder structure ++- **Artist Deduplication**: Merges local and streaming provider artists to avoid duplicates in search results ++- **Album Enrichment**: Local albums are enriched with missing tracks from streaming providers ++- **Cover Art Proxy**: Serves cover art for external content transparently ++ ++## Compatible Clients ++ ++### PC ++ ++- [Aonsoku](https://github.com/victoralvesf/aonsoku) ++- [Feishin](https://github.com/jeffvli/feishin) ++- [Subplayer](https://github.com/peguerosdc/subplayer) ++- [Aurial](https://github.com/shrimpza/aurial) ++ ++### Android ++ ++- [Tempus](https://github.com/eddyizm/tempus) ++- [Substreamer](https://substreamerapp.com/) ++ ++### iOS ++ ++- [Narjo](https://www.reddit.com/r/NarjoApp/) ++- [Arpeggi](https://www.reddit.com/r/arpeggiApp/) ++ ++> **Want to improve client compatibility?** Pull requests are welcome! ++ ++### Incompatible Clients ++ ++These clients are **not compatible** with octo-fiesta due to architectural limitations: ++ ++- [Symfonium](https://symfonium.app/) - Uses offline-first architecture and never queries the server for searches, making streaming provider integration impossible. [See details](https://support.symfonium.app/t/suggestions-on-search-function/1121/) ++ ++## Supported Music Providers ++ ++- **[SquidWTF](https://tidal.squid.wtf/)** - Quality: FLAC (Hi-Res 24-bit/192kHz & CD-Lossless 16-bit/44.1kHz), AAC ++- **[Deezer](https://www.deezer.com/)** - Quality: FLAC, MP3_320, MP3_128 ++- **[Qobuz](https://www.qobuz.com/)** - Quality: FLAC, FLAC_24_HIGH (Hi-Res 24-bit/192kHz), FLAC_24_LOW, FLAC_16, MP3_320 ++ ++Choose your preferred provider via the `MUSIC_SERVICE` environment variable. Additional providers may be added in future releases. ++ ++## Requirements ++ ++- A running Subsonic-compatible server (developed and tested with [Navidrome](https://www.navidrome.org/)) ++- Credentials for at least one music provider (IF NOT USING SQUIDWTF): ++ - **Deezer**: ARL token from browser cookies ++ - **Qobuz**: User ID + User Auth Token from browser localStorage ([see Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token))) ++- Docker and Docker Compose (recommended) **or** [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) for manual installation ++ ++## Quick Start (Docker) ++ ++The easiest way to run Octo-Fiestarr is with Docker Compose. ++ ++1. **Create your environment file** ++ ```bash ++ cp .env.example .env ++ ``` ++ ++2. **Edit the `.env` file** with your configuration: ++ ```bash ++ # Navidrome/Subsonic server URL ++ SUBSONIC_URL=http://localhost:4533 ++ ++ # Path where downloaded songs will be stored on the host (only applies if STORAGE_MODE=Permanent) ++ DOWNLOAD_PATH=./downloads ++ ++ # Music service to use: SquidWTF, Deezer, or Qobuz (default: SquidWTF) ++ MUSIC_SERVICE=SquidWTF ++ ++ # ===== SquidWTF CONFIGURATION ===== ++ # Different quality options for SquidWTF. Only FLAC supported right now ++ SQUIDWTF_QUALITY=FLAC ++ ++ # ===== DEEZER CONFIGURATION ===== ++ # Deezer ARL token (required if using Deezer) ++ # See README.md for instructions on how to get this token ++ DEEZER_ARL=your-deezer-arl-token ++ ++ # Fallback ARL token (optional) ++ DEEZER_ARL_FALLBACK= ++ ++ # Preferred audio quality: FLAC, MP3_320, MP3_128 (optional) ++ # If not specified, the highest available quality for your account will be used ++ DEEZER_QUALITY= ++ ++ # ===== QOBUZ CONFIGURATION ===== ++ # Qobuz user authentication token (required if using Qobuz) ++ # Get this from your browser after logging into play.qobuz.com ++ # See README.md for detailed instructions ++ QOBUZ_USER_AUTH_TOKEN= ++ ++ # Qobuz user ID (required if using Qobuz) ++ # Get this from your browser after logging into play.qobuz.com ++ QOBUZ_USER_ID= ++ ++ # Preferred audio quality: FLAC, FLAC_24_HIGH, FLAC_24_LOW, FLAC_16, MP3_320 (optional) ++ # If not specified, the highest available quality will be used ++ QOBUZ_QUALITY= ++ ++ # ===== GENERAL SETTINGS ===== ++ # External playlists support (optional, default: true) ++ # When enabled, allows searching and downloading playlists from Deezer/Qobuz ++ # Starring a playlist triggers automatic download of all tracks and creates an M3U file ++ ENABLE_EXTERNAL_PLAYLISTS=true ++ ++ # Playlists directory name (optional, default: playlists) ++ # M3U playlist files will be created in {DOWNLOAD_PATH}/{PLAYLISTS_DIRECTORY}/ ++ PLAYLISTS_DIRECTORY=playlists ++ ++ # Explicit content filter (optional, default: All) ++ # - All: Show all tracks (no filtering) ++ # - ExplicitOnly: Exclude clean/edited versions, keep original explicit content ++ # - CleanOnly: Only show clean content (naturally clean or edited versions) ++ # Note: This only works with Deezer, Qobuz doesn't expose explicit content flags ++ EXPLICIT_FILTER=All ++ ++ # Download mode (optional, default: Track) ++ # - Track: Download only the played track ++ # - Album: When playing a track, download the entire album in background ++ # The played track is downloaded first, remaining tracks are queued ++ DOWNLOAD_MODE=Track ++ ++ # Storage mode (optional, default: Permanent) ++ # - Permanent: Files are saved to the library permanently and registered in Navidrome ++ # - Cache: Files are stored in /tmp and automatically cleaned up after CACHE_DURATION_HOURS ++ # Not registered in Navidrome, ideal for streaming without library bloat ++ # Note: On Linux/Docker, you can customize cache location by setting TMPDIR environment variable ++ STORAGE_MODE=Permanent ++ ++ # Cache duration in hours (optional, default: 1) ++ # Files older than this duration will be automatically deleted when STORAGE_MODE=Cache ++ # Based on last access time (updated each time the file is streamed) ++ # Cache location: /tmp/octo-fiesta-cache (or $TMPDIR/octo-fiesta-cache if TMPDIR is set) ++ CACHE_DURATION_HOURS=1 ++ ``` ++ ++3. **Start the container** ++ ```bash ++ docker-compose up -d ++ ``` ++ ++ The proxy will be available at `http://localhost:5274`. ++ ++4. **Configure your Subsonic client** ++ ++ Point your Subsonic client to `http://localhost:5274` instead of your Navidrome server directly. ++ ++> **Tip**: Make sure the `DOWNLOAD_PATH` points to a directory that Navidrome can scan, so downloaded songs appear in your library. ++ ++## Configuration ++ ++### General Settings ++ ++| Setting | Description | ++|---------|-------------| ++| `Subsonic:Url` | URL of your Navidrome/Subsonic server | ++| `Subsonic:MusicService` | Music provider to use: `SquidWTF`, `Deezer`, or `Qobuz` (default: `SquidWTF`) | ++| `Library:DownloadPath` | Directory where downloaded songs are stored | ++ ++### SquidWTF Settings ++ ++| Setting | Description | ++|---------|-------------| ++| `SquidWTF:Quality` | Preferred audio quality: `FLAC`, `MP3_320`, `MP3_128`. If not specified, the highest available quality for your account will be used | ++ ++### Deezer Settings ++ ++| Setting | Description | ++|---------|-------------| ++| `Deezer:Arl` | Your Deezer ARL token (required if using Deezer) | ++| `Deezer:ArlFallback` | Backup ARL token if primary fails | ++| `Deezer:Quality` | Preferred audio quality: `FLAC`, `MP3_320`, `MP3_128`. If not specified, the highest available quality for your account will be used | ++ ++### Qobuz Settings ++ ++| Setting | Description | ++|---------|-------------| ++| `Qobuz:UserAuthToken` | Your Qobuz User Auth Token (required if using Qobuz) - [How to get it](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token)) | ++| `Qobuz:UserId` | Your Qobuz User ID (required if using Qobuz) | ++| `Qobuz:Quality` | Preferred audio quality: `FLAC`, `FLAC_24_HIGH`, `FLAC_24_LOW`, `FLAC_16`, `MP3_320`. If not specified, the highest available quality will be used | ++ ++### External Playlists ++ ++Octo-Fiesta supports discovering and downloading playlists from your streaming providers (SquidWTF, Deezer, and Qobuz). ++ ++| Setting | Description | ++|---------|-------------| ++| `Subsonic:EnableExternalPlaylists` | Enable/disable external playlist support (default: `true`) | ++| `Subsonic:PlaylistsDirectory` | Directory name where M3U playlist files are created (default: `playlists`) | ++ ++**How it works:** ++1. Search for playlists from an external provider using the global search in your Subsonic client ++2. When you "star" (favorite) a playlist, Octo-Fiesta automatically downloads all tracks ++3. An M3U playlist file is created in `{DownloadPath}/playlists/` with relative paths to downloaded tracks ++4. Individual tracks are added to the M3U as they are played or downloaded ++ ++**Environment variable:** ++```bash ++# To disable playlists ++Subsonic__EnableExternalPlaylists=false ++``` ++ ++> **Note**: Due to client-side filtering, playlists from streaming providers may not appear in the "Playlists" tab of some clients, but will show up in global search results. ++ ++### Getting Credentials ++ ++#### Deezer ARL Token ++ ++See the [Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Deezer-Credentials-(ARL-Token)) for detailed instructions on obtaining your Deezer ARL token. ++ ++#### Qobuz Credentials ++ ++See the [Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token)) for detailed instructions on obtaining your Qobuz User ID and User Auth Token. ++ ++## Limitations ++ ++- **Playlist Search**: Subsonic clients like Aonsoku filter playlists client-side from a cached `getPlaylists` call. Streaming provider playlists appear in global search (`search3`) but not in the Playlists tab filter. ++- **Region Restrictions**: Some tracks may be unavailable depending on your region and provider. ++- **Token Expiration**: Provider authentication tokens expire and need periodic refresh. ++ ++## Architecture ++ ++``` ++┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ ++│ Subsonic │────▶│ Octo-Fiesta │────▶│ Navidrome │ ++│ Client │◀────│ (Proxy) │◀────│ Server │ ++│ (Aonsoku) │ │ │ │ │ ++└─────────────────┘ └────────┬─────────┘ └─────────────────┘ ++ │ ++ ▼ ++ ┌─────────────────┐ ++ │ Music Providers │ ++ │ - Deezer │ ++ │ - Qobuz │ ++ │ - (more...) │ ++ └─────────────────┘ ++``` ++ ++## Manual Installation ++ ++If you prefer to run Octo-Fiesta without Docker: ++ ++1. **Clone the repository** ++ ```bash ++ git clone https://github.com/your-username/octo-fiesta.git ++ cd octo-fiesta ++ ``` ++ ++2. **Restore dependencies** ++ ```bash ++ dotnet restore ++ ``` ++ ++3. **Configure the application** ++ ++ Edit `octo-fiesta/appsettings.json`: ++ ```json ++{ ++ "Subsonic": { ++ "Url": "https://navidrome.local.bransonb.com", ++ "MusicService": "SquidWTF", ++ "ExplicitFilter": "All", ++ "DownloadMode": "Track", ++ "StorageMode": "Permanent", ++ "CacheDurationHours": 1 ++ }, ++ "Library": { ++ "DownloadPath": "./downloads" ++ }, ++ "Qobuz": { ++ "UserAuthToken": "your-qobuz-token", ++ "UserId": "your-qobuz-user-id", ++ "Quality": "FLAC" ++ }, ++ "Deezer": { ++ "Arl": "your-deezer-arl-token", ++ "ArlFallback": "", ++ "Quality": "FLAC" ++ }, ++ "SquidWTF": { ++ "Quality": "FLAC" ++ } ++} ++``` ++ ++4. **Run the server** ++ ```bash ++ cd octo-fiesta ++ dotnet run ++ ``` ++ ++ The proxy will start on `http://localhost:5274` by default. ++ ++5. **Configure your Subsonic client** ++ ++ Point your Subsonic client to `http://localhost:5274` instead of your Navidrome server directly. ++ ++## API Endpoints ++ ++The proxy implements the Subsonic API and adds transparent streaming provider integration to: ++ ++| Endpoint | Description | ++|----------|-------------| ++| `GET /rest/search3` | Merged search results from Navidrome + streaming provider (including playlists) | ++| `GET /rest/stream` | Streams audio, downloading from provider if needed | ++| `GET /rest/getSong` | Returns song details (local or from provider) | ++| `GET /rest/getAlbum` | Returns album with tracks from both sources | ++| `GET /rest/getArtist` | Returns artist with albums from both sources | ++| `GET /rest/getCoverArt` | Proxies cover art for external content | ++| `GET /rest/star` | Stars items; triggers automatic playlist download for external playlists | ++ ++All other Subsonic API endpoints are passed through to Navidrome unchanged. ++ ++## External ID Format ++ ++External (streaming provider) content uses typed IDs: ++ ++| Type | Format | Example | ++|------|--------|---------| ++| Song | `ext-{provider}-song-{id}` | `ext-deezer-song-123456`, `ext-qobuz-song-789012` | ++| Album | `ext-{provider}-album-{id}` | `ext-deezer-album-789012`, `ext-qobuz-album-456789` | ++| Artist | `ext-{provider}-artist-{id}` | `ext-deezer-artist-259`, `ext-qobuz-artist-123` | ++ ++Legacy format `ext-deezer-{id}` is also supported (assumes song type). ++ ++## Download Folder Structure ++ ++Downloaded music is organized as: ++``` ++downloads/ ++├── Artist Name/ ++│ ├── Album Title/ ++│ │ ├── 01 - Track One.mp3 ++│ │ ├── 02 - Track Two.mp3 ++│ │ └── ... ++│ └── Another Album/ ++│ └── ... ++├── Another Artist/ ++│ └── ... ++└── playlists/ ++ ├── My Favorite Songs.m3u ++ ├── Chill Vibes.m3u ++ └── ... ++``` ++ ++Playlists are stored as M3U files with relative paths to downloaded tracks, making them portable and compatible with most music players. ++ ++## Metadata Embedding ++ ++Downloaded files include: ++- **Basic**: Title, Artist, Album, Album Artist ++- **Track Info**: Track Number, Total Tracks, Disc Number ++- **Dates**: Year, Release Date ++- **Audio**: BPM, Duration ++- **Identifiers**: ISRC (in comments) ++- **Credits**: Contributors/Composers ++- **Visual**: Embedded cover art (high resolution) ++- **Rights**: Copyright, Label ++ ++## Development ++ ++### Build ++```bash ++dotnet build ++``` ++ ++### Run Tests ++```bash ++dotnet test ++``` ++ ++### Project Structure ++ ++``` ++octo-fiesta/ ++├── Controllers/ ++│ └── SubsonicController.cs # Main API controller ++├── Middleware/ ++│ └── GlobalExceptionHandler.cs # Global error handling ++├── Models/ ++│ ├── Domain/ # Domain entities ++│ │ ├── Song.cs ++│ │ ├── Album.cs ++│ │ └── Artist.cs ++│ ├── Settings/ # Configuration models ++│ │ ├── SubsonicSettings.cs ++│ │ ├── DeezerSettings.cs ++│ │ └── QobuzSettings.cs ++│ ├── Download/ # Download-related models ++│ │ ├── DownloadInfo.cs ++│ │ └── DownloadStatus.cs ++│ ├── Search/ ++│ │ └── SearchResult.cs ++│ └── Subsonic/ ++│ └── ScanStatus.cs ++├── Services/ ++│ ├── Common/ # Shared services ++│ │ ├── BaseDownloadService.cs # Template method base class ++│ │ ├── PathHelper.cs # Path utilities ++│ │ ├── Result.cs # Result pattern ++│ │ └── Error.cs # Error types ++│ ├── Deezer/ # Deezer provider ++│ │ ├── DeezerDownloadService.cs ++│ │ ├── DeezerMetadataService.cs ++│ │ └── DeezerStartupValidator.cs ++│ ├── Qobuz/ # Qobuz provider ++│ │ ├── QobuzDownloadService.cs ++│ │ ├── QobuzMetadataService.cs ++│ │ ├── QobuzBundleService.cs ++│ │ └── QobuzStartupValidator.cs ++│ ├── Local/ # Local library ++│ │ ├── ILocalLibraryService.cs ++│ │ └── LocalLibraryService.cs ++│ ├── Subsonic/ # Subsonic API logic ++│ │ ├── SubsonicProxyService.cs # Request proxying ++│ │ ├── SubsonicModelMapper.cs # Model mapping ++│ │ ├── SubsonicRequestParser.cs # Request parsing ++│ │ └── SubsonicResponseBuilder.cs # Response building ++│ ├── Validation/ # Startup validation ++│ │ ├── IStartupValidator.cs ++│ │ ├── BaseStartupValidator.cs ++│ │ ├── SubsonicStartupValidator.cs ++│ │ ├── StartupValidationOrchestrator.cs ++│ │ └── ValidationResult.cs ++│ ├── IDownloadService.cs # Download interface ++│ ├── IMusicMetadataService.cs # Metadata interface ++│ └── StartupValidationService.cs ++├── Program.cs # Application entry point ++└── appsettings.json # Configuration ++ ++octo-fiesta.Tests/ ++├── DeezerDownloadServiceTests.cs # Deezer download tests ++├── DeezerMetadataServiceTests.cs # Deezer metadata tests ++├── QobuzDownloadServiceTests.cs # Qobuz download tests (127 tests) ++├── LocalLibraryServiceTests.cs # Local library tests ++├── SubsonicModelMapperTests.cs # Model mapping tests ++├── SubsonicProxyServiceTests.cs # Proxy service tests ++├── SubsonicRequestParserTests.cs # Request parser tests ++└── SubsonicResponseBuilderTests.cs # Response builder tests ++``` ++ ++### Dependencies ++ ++- **BouncyCastle.Cryptography** - Blowfish decryption for Deezer streams ++- **TagLibSharp** - ID3 tag and cover art embedding ++- **Swashbuckle.AspNetCore** - Swagger/OpenAPI documentation ++- **xUnit** - Unit testing framework ++- **Moq** - Mocking library for tests ++- **FluentAssertions** - Fluent assertion library for tests ++ ++## License ++ ++GPL-3.0 ++ ++## Acknowledgments ++ ++- [Navidrome](https://www.navidrome.org/) - The excellent self-hosted music server ++- [Deezer](https://www.deezer.com/) - Music streaming service ++- [Qobuz](https://www.qobuz.com/) - Hi-Res music streaming service ++- [Subsonic API](http://www.subsonic.org/pages/api.jsp) - The API specification +diff --git a/originals/octo-fiestarr/docker-compose.yml b/originals/octo-fiestarr/docker-compose.yml +new file mode 100644 +index 0000000..f9b07e9 +--- /dev/null ++++ b/originals/octo-fiestarr/docker-compose.yml +@@ -0,0 +1,25 @@ ++services: ++ octo-fiestarr: ++ image: ghcr.io/bransoned/octo-fiestarr ++ container_name: octo-fiestarr ++ restart: unless-stopped ++ ports: ++ - "5274:8080" ++ environment: ++ - ASPNETCORE_ENVIRONMENT=Production ++ # Navidrome/Subsonic server URL ++ - Subsonic__Url=${SUBSONIC_URL:-http://localhost:4533} ++ # Explicit content filter: All, ExplicitOnly, CleanOnly (default: ExplicitOnly) ++ - Subsonic__ExplicitFilter=${EXPLICIT_FILTER:-ExplicitOnly} ++ # Download mode: Track (only requested track), Album (full album when playing a track) ++ - Subsonic__DownloadMode=${DOWNLOAD_MODE:-Track} ++ # Download path inside container ++ - Library__DownloadPath=/app/downloads ++ # SquidWTF preferred audio quality ++ - SquidWTF__Quality=${SQUIDWTF_QUALITY:-FLAC} ++ # Set cache or permanent download of external songs ++ - Storage__Mode=${STORAGE_MODE:-Permanent} ++ # Set cache duration ++ - Cache__DurationHours=${CACHE_DURATION_HOURS:-1} ++ volumes: ++ - ${DOWNLOAD_PATH:-./downloads}:/app/downloads +diff --git a/octo-fiesta.Tests/DeezerDownloadServiceTests.cs b/originals/octo-fiestarr/octo-fiesta.Tests/DeezerDownloadServiceTests.cs +similarity index 100% +rename from octo-fiesta.Tests/DeezerDownloadServiceTests.cs +rename to originals/octo-fiestarr/octo-fiesta.Tests/DeezerDownloadServiceTests.cs +diff --git a/octo-fiesta.Tests/DeezerMetadataServiceTests.cs b/originals/octo-fiestarr/octo-fiesta.Tests/DeezerMetadataServiceTests.cs +similarity index 100% +rename from octo-fiesta.Tests/DeezerMetadataServiceTests.cs +rename to originals/octo-fiestarr/octo-fiesta.Tests/DeezerMetadataServiceTests.cs +diff --git a/octo-fiesta.Tests/LocalLibraryServiceTests.cs b/originals/octo-fiestarr/octo-fiesta.Tests/LocalLibraryServiceTests.cs +similarity index 100% +rename from octo-fiesta.Tests/LocalLibraryServiceTests.cs +rename to originals/octo-fiestarr/octo-fiesta.Tests/LocalLibraryServiceTests.cs +diff --git a/octo-fiesta.Tests/PlaylistIdHelperTests.cs b/originals/octo-fiestarr/octo-fiesta.Tests/PlaylistIdHelperTests.cs +similarity index 100% +rename from octo-fiesta.Tests/PlaylistIdHelperTests.cs +rename to originals/octo-fiestarr/octo-fiesta.Tests/PlaylistIdHelperTests.cs +diff --git a/octo-fiesta.Tests/QobuzDownloadServiceTests.cs b/originals/octo-fiestarr/octo-fiesta.Tests/QobuzDownloadServiceTests.cs +similarity index 100% +rename from octo-fiesta.Tests/QobuzDownloadServiceTests.cs +rename to originals/octo-fiestarr/octo-fiesta.Tests/QobuzDownloadServiceTests.cs +diff --git a/octo-fiesta.Tests/QobuzMetadataServiceTests.cs b/originals/octo-fiestarr/octo-fiesta.Tests/QobuzMetadataServiceTests.cs +similarity index 100% +rename from octo-fiesta.Tests/QobuzMetadataServiceTests.cs +rename to originals/octo-fiestarr/octo-fiesta.Tests/QobuzMetadataServiceTests.cs +diff --git a/octo-fiesta.Tests/SubsonicModelMapperTests.cs b/originals/octo-fiestarr/octo-fiesta.Tests/SubsonicModelMapperTests.cs +similarity index 100% +rename from octo-fiesta.Tests/SubsonicModelMapperTests.cs +rename to originals/octo-fiestarr/octo-fiesta.Tests/SubsonicModelMapperTests.cs +diff --git a/octo-fiesta.Tests/SubsonicProxyServiceTests.cs b/originals/octo-fiestarr/octo-fiesta.Tests/SubsonicProxyServiceTests.cs +similarity index 100% +rename from octo-fiesta.Tests/SubsonicProxyServiceTests.cs +rename to originals/octo-fiestarr/octo-fiesta.Tests/SubsonicProxyServiceTests.cs +diff --git a/octo-fiesta.Tests/SubsonicRequestParserTests.cs b/originals/octo-fiestarr/octo-fiesta.Tests/SubsonicRequestParserTests.cs +similarity index 100% +rename from octo-fiesta.Tests/SubsonicRequestParserTests.cs +rename to originals/octo-fiestarr/octo-fiesta.Tests/SubsonicRequestParserTests.cs +diff --git a/octo-fiesta.Tests/SubsonicResponseBuilderTests.cs b/originals/octo-fiestarr/octo-fiesta.Tests/SubsonicResponseBuilderTests.cs +similarity index 100% +rename from octo-fiesta.Tests/SubsonicResponseBuilderTests.cs +rename to originals/octo-fiestarr/octo-fiesta.Tests/SubsonicResponseBuilderTests.cs +diff --git a/octo-fiesta.Tests/octo-fiesta.Tests.csproj b/originals/octo-fiestarr/octo-fiesta.Tests/octo-fiesta.Tests.csproj +similarity index 100% +rename from octo-fiesta.Tests/octo-fiesta.Tests.csproj +rename to originals/octo-fiestarr/octo-fiesta.Tests/octo-fiesta.Tests.csproj +diff --git a/octo-fiesta.sln b/originals/octo-fiestarr/octo-fiesta.sln +similarity index 100% +rename from octo-fiesta.sln +rename to originals/octo-fiestarr/octo-fiesta.sln +diff --git a/octo-fiesta/Controllers/SubSonicController.cs b/originals/octo-fiestarr/octo-fiesta/Controllers/SubSonicController.cs +similarity index 100% +rename from octo-fiesta/Controllers/SubSonicController.cs +rename to originals/octo-fiestarr/octo-fiesta/Controllers/SubSonicController.cs +diff --git a/octo-fiesta/Middleware/GlobalExceptionHandler.cs b/originals/octo-fiestarr/octo-fiesta/Middleware/GlobalExceptionHandler.cs +similarity index 100% +rename from octo-fiesta/Middleware/GlobalExceptionHandler.cs +rename to originals/octo-fiestarr/octo-fiesta/Middleware/GlobalExceptionHandler.cs +diff --git a/octo-fiesta/Models/Domain/Album.cs b/originals/octo-fiestarr/octo-fiesta/Models/Domain/Album.cs +similarity index 100% +rename from octo-fiesta/Models/Domain/Album.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Domain/Album.cs +diff --git a/octo-fiesta/Models/Domain/Artist.cs b/originals/octo-fiestarr/octo-fiesta/Models/Domain/Artist.cs +similarity index 100% +rename from octo-fiesta/Models/Domain/Artist.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Domain/Artist.cs +diff --git a/octo-fiesta/Models/Domain/Song.cs b/originals/octo-fiestarr/octo-fiesta/Models/Domain/Song.cs +similarity index 100% +rename from octo-fiesta/Models/Domain/Song.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Domain/Song.cs +diff --git a/octo-fiesta/Models/Download/DownloadInfo.cs b/originals/octo-fiestarr/octo-fiesta/Models/Download/DownloadInfo.cs +similarity index 100% +rename from octo-fiesta/Models/Download/DownloadInfo.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Download/DownloadInfo.cs +diff --git a/octo-fiesta/Models/Download/DownloadStatus.cs b/originals/octo-fiestarr/octo-fiesta/Models/Download/DownloadStatus.cs +similarity index 100% +rename from octo-fiesta/Models/Download/DownloadStatus.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Download/DownloadStatus.cs +diff --git a/octo-fiesta/Models/Search/SearchResult.cs b/originals/octo-fiestarr/octo-fiesta/Models/Search/SearchResult.cs +similarity index 100% +rename from octo-fiesta/Models/Search/SearchResult.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Search/SearchResult.cs +diff --git a/octo-fiesta/Models/Settings/DeezerSettings.cs b/originals/octo-fiestarr/octo-fiesta/Models/Settings/DeezerSettings.cs +similarity index 100% +rename from octo-fiesta/Models/Settings/DeezerSettings.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Settings/DeezerSettings.cs +diff --git a/octo-fiesta/Models/Settings/QobuzSettings.cs b/originals/octo-fiestarr/octo-fiesta/Models/Settings/QobuzSettings.cs +similarity index 100% +rename from octo-fiesta/Models/Settings/QobuzSettings.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Settings/QobuzSettings.cs +diff --git a/octo-fiesta/Models/Settings/SquidWTFSettings.cs b/originals/octo-fiestarr/octo-fiesta/Models/Settings/SquidWTFSettings.cs +similarity index 100% +rename from octo-fiesta/Models/Settings/SquidWTFSettings.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Settings/SquidWTFSettings.cs +diff --git a/octo-fiesta/Models/Settings/SubsonicSettings.cs b/originals/octo-fiestarr/octo-fiesta/Models/Settings/SubsonicSettings.cs +similarity index 100% +rename from octo-fiesta/Models/Settings/SubsonicSettings.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Settings/SubsonicSettings.cs +diff --git a/octo-fiesta/Models/Subsonic/ExternalPlaylist.cs b/originals/octo-fiestarr/octo-fiesta/Models/Subsonic/ExternalPlaylist.cs +similarity index 100% +rename from octo-fiesta/Models/Subsonic/ExternalPlaylist.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Subsonic/ExternalPlaylist.cs +diff --git a/octo-fiesta/Models/Subsonic/ScanStatus.cs b/originals/octo-fiestarr/octo-fiesta/Models/Subsonic/ScanStatus.cs +similarity index 100% +rename from octo-fiesta/Models/Subsonic/ScanStatus.cs +rename to originals/octo-fiestarr/octo-fiesta/Models/Subsonic/ScanStatus.cs +diff --git a/octo-fiesta/Program.cs b/originals/octo-fiestarr/octo-fiesta/Program.cs +similarity index 100% +rename from octo-fiesta/Program.cs +rename to originals/octo-fiestarr/octo-fiesta/Program.cs +diff --git a/originals/octo-fiestarr/octo-fiesta/Properties/launchSettings.json b/originals/octo-fiestarr/octo-fiesta/Properties/launchSettings.json +new file mode 100644 +index 0000000..ccc0a16 +--- /dev/null ++++ b/originals/octo-fiestarr/octo-fiesta/Properties/launchSettings.json +@@ -0,0 +1,25 @@ ++{ ++ "$schema": "https://json.schemastore.org/launchsettings.json", ++ "profiles": { ++ "http": { ++ "commandName": "Project", ++ "dotnetRunMessages": true, ++ "launchBrowser": true, ++ "launchUrl": "swagger", ++ "applicationUrl": "http://localhost:5274", ++ "environmentVariables": { ++ "ASPNETCORE_ENVIRONMENT": "Production" ++ } ++ }, ++ "https": { ++ "commandName": "Project", ++ "dotnetRunMessages": true, ++ "launchBrowser": true, ++ "launchUrl": "swagger", ++ "applicationUrl": "https://localhost:7248;http://localhost:5274", ++ "environmentVariables": { ++ "ASPNETCORE_ENVIRONMENT": "Production" ++ } ++ } ++ } ++} +diff --git a/octo-fiesta/Services/Common/BaseDownloadService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Common/BaseDownloadService.cs +similarity index 100% +rename from octo-fiesta/Services/Common/BaseDownloadService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Common/BaseDownloadService.cs +diff --git a/octo-fiesta/Services/Common/CacheCleanupService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Common/CacheCleanupService.cs +similarity index 100% +rename from octo-fiesta/Services/Common/CacheCleanupService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Common/CacheCleanupService.cs +diff --git a/octo-fiesta/Services/Common/Error.cs b/originals/octo-fiestarr/octo-fiesta/Services/Common/Error.cs +similarity index 100% +rename from octo-fiesta/Services/Common/Error.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Common/Error.cs +diff --git a/octo-fiesta/Services/Common/PathHelper.cs b/originals/octo-fiestarr/octo-fiesta/Services/Common/PathHelper.cs +similarity index 100% +rename from octo-fiesta/Services/Common/PathHelper.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Common/PathHelper.cs +diff --git a/octo-fiesta/Services/Common/PlaylistIdHelper.cs b/originals/octo-fiestarr/octo-fiesta/Services/Common/PlaylistIdHelper.cs +similarity index 100% +rename from octo-fiesta/Services/Common/PlaylistIdHelper.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Common/PlaylistIdHelper.cs +diff --git a/octo-fiesta/Services/Common/Result.cs b/originals/octo-fiestarr/octo-fiesta/Services/Common/Result.cs +similarity index 100% +rename from octo-fiesta/Services/Common/Result.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Common/Result.cs +diff --git a/octo-fiesta/Services/Deezer/DeezerDownloadService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Deezer/DeezerDownloadService.cs +similarity index 100% +rename from octo-fiesta/Services/Deezer/DeezerDownloadService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Deezer/DeezerDownloadService.cs +diff --git a/octo-fiesta/Services/Deezer/DeezerMetadataService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Deezer/DeezerMetadataService.cs +similarity index 100% +rename from octo-fiesta/Services/Deezer/DeezerMetadataService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Deezer/DeezerMetadataService.cs +diff --git a/octo-fiesta/Services/Deezer/DeezerStartupValidator.cs b/originals/octo-fiestarr/octo-fiesta/Services/Deezer/DeezerStartupValidator.cs +similarity index 100% +rename from octo-fiesta/Services/Deezer/DeezerStartupValidator.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Deezer/DeezerStartupValidator.cs +diff --git a/octo-fiesta/Services/IDownloadService.cs b/originals/octo-fiestarr/octo-fiesta/Services/IDownloadService.cs +similarity index 100% +rename from octo-fiesta/Services/IDownloadService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/IDownloadService.cs +diff --git a/octo-fiesta/Services/IMusicMetadataService.cs b/originals/octo-fiestarr/octo-fiesta/Services/IMusicMetadataService.cs +similarity index 100% +rename from octo-fiesta/Services/IMusicMetadataService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/IMusicMetadataService.cs +diff --git a/octo-fiesta/Services/Local/ILocalLibraryService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Local/ILocalLibraryService.cs +similarity index 100% +rename from octo-fiesta/Services/Local/ILocalLibraryService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Local/ILocalLibraryService.cs +diff --git a/octo-fiesta/Services/Local/LocalLibraryService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Local/LocalLibraryService.cs +similarity index 100% +rename from octo-fiesta/Services/Local/LocalLibraryService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Local/LocalLibraryService.cs +diff --git a/octo-fiesta/Services/Qobuz/QobuzBundleService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Qobuz/QobuzBundleService.cs +similarity index 100% +rename from octo-fiesta/Services/Qobuz/QobuzBundleService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Qobuz/QobuzBundleService.cs +diff --git a/octo-fiesta/Services/Qobuz/QobuzDownloadService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Qobuz/QobuzDownloadService.cs +similarity index 100% +rename from octo-fiesta/Services/Qobuz/QobuzDownloadService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Qobuz/QobuzDownloadService.cs +diff --git a/octo-fiesta/Services/Qobuz/QobuzMetadataService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Qobuz/QobuzMetadataService.cs +similarity index 100% +rename from octo-fiesta/Services/Qobuz/QobuzMetadataService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Qobuz/QobuzMetadataService.cs +diff --git a/octo-fiesta/Services/Qobuz/QobuzStartupValidator.cs b/originals/octo-fiestarr/octo-fiesta/Services/Qobuz/QobuzStartupValidator.cs +similarity index 100% +rename from octo-fiesta/Services/Qobuz/QobuzStartupValidator.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Qobuz/QobuzStartupValidator.cs +diff --git a/octo-fiesta/Services/SquidWTF/SquidWTFDownloadService.cs b/originals/octo-fiestarr/octo-fiesta/Services/SquidWTF/SquidWTFDownloadService.cs +similarity index 100% +rename from octo-fiesta/Services/SquidWTF/SquidWTFDownloadService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/SquidWTF/SquidWTFDownloadService.cs +diff --git a/octo-fiesta/Services/SquidWTF/SquidWTFMetadataService.cs b/originals/octo-fiestarr/octo-fiesta/Services/SquidWTF/SquidWTFMetadataService.cs +similarity index 100% +rename from octo-fiesta/Services/SquidWTF/SquidWTFMetadataService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/SquidWTF/SquidWTFMetadataService.cs +diff --git a/octo-fiesta/Services/SquidWTF/SquidWTFStartupValidator.cs b/originals/octo-fiestarr/octo-fiesta/Services/SquidWTF/SquidWTFStartupValidator.cs +similarity index 100% +rename from octo-fiesta/Services/SquidWTF/SquidWTFStartupValidator.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/SquidWTF/SquidWTFStartupValidator.cs +diff --git a/octo-fiesta/Services/StartupValidationService.cs b/originals/octo-fiestarr/octo-fiesta/Services/StartupValidationService.cs +similarity index 100% +rename from octo-fiesta/Services/StartupValidationService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/StartupValidationService.cs +diff --git a/octo-fiesta/Services/Subsonic/PlaylistSyncService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Subsonic/PlaylistSyncService.cs +similarity index 100% +rename from octo-fiesta/Services/Subsonic/PlaylistSyncService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Subsonic/PlaylistSyncService.cs +diff --git a/octo-fiesta/Services/Subsonic/SubsonicModelMapper.cs b/originals/octo-fiestarr/octo-fiesta/Services/Subsonic/SubsonicModelMapper.cs +similarity index 100% +rename from octo-fiesta/Services/Subsonic/SubsonicModelMapper.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Subsonic/SubsonicModelMapper.cs +diff --git a/octo-fiesta/Services/Subsonic/SubsonicProxyService.cs b/originals/octo-fiestarr/octo-fiesta/Services/Subsonic/SubsonicProxyService.cs +similarity index 100% +rename from octo-fiesta/Services/Subsonic/SubsonicProxyService.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Subsonic/SubsonicProxyService.cs +diff --git a/octo-fiesta/Services/Subsonic/SubsonicRequestParser.cs b/originals/octo-fiestarr/octo-fiesta/Services/Subsonic/SubsonicRequestParser.cs +similarity index 100% +rename from octo-fiesta/Services/Subsonic/SubsonicRequestParser.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Subsonic/SubsonicRequestParser.cs +diff --git a/octo-fiesta/Services/Subsonic/SubsonicResponseBuilder.cs b/originals/octo-fiestarr/octo-fiesta/Services/Subsonic/SubsonicResponseBuilder.cs +similarity index 100% +rename from octo-fiesta/Services/Subsonic/SubsonicResponseBuilder.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Subsonic/SubsonicResponseBuilder.cs +diff --git a/octo-fiesta/Services/Validation/BaseStartupValidator.cs b/originals/octo-fiestarr/octo-fiesta/Services/Validation/BaseStartupValidator.cs +similarity index 100% +rename from octo-fiesta/Services/Validation/BaseStartupValidator.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Validation/BaseStartupValidator.cs +diff --git a/octo-fiesta/Services/Validation/IStartupValidator.cs b/originals/octo-fiestarr/octo-fiesta/Services/Validation/IStartupValidator.cs +similarity index 100% +rename from octo-fiesta/Services/Validation/IStartupValidator.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Validation/IStartupValidator.cs +diff --git a/octo-fiesta/Services/Validation/StartupValidationOrchestrator.cs b/originals/octo-fiestarr/octo-fiesta/Services/Validation/StartupValidationOrchestrator.cs +similarity index 100% +rename from octo-fiesta/Services/Validation/StartupValidationOrchestrator.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Validation/StartupValidationOrchestrator.cs +diff --git a/octo-fiesta/Services/Validation/SubsonicStartupValidator.cs b/originals/octo-fiestarr/octo-fiesta/Services/Validation/SubsonicStartupValidator.cs +similarity index 100% +rename from octo-fiesta/Services/Validation/SubsonicStartupValidator.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Validation/SubsonicStartupValidator.cs +diff --git a/octo-fiesta/Services/Validation/ValidationResult.cs b/originals/octo-fiestarr/octo-fiesta/Services/Validation/ValidationResult.cs +similarity index 100% +rename from octo-fiesta/Services/Validation/ValidationResult.cs +rename to originals/octo-fiestarr/octo-fiesta/Services/Validation/ValidationResult.cs +diff --git a/originals/octo-fiestarr/octo-fiesta/appsettings.Development.json b/originals/octo-fiestarr/octo-fiesta/appsettings.Development.json +new file mode 100644 +index 0000000..ff66ba6 +--- /dev/null ++++ b/originals/octo-fiestarr/octo-fiesta/appsettings.Development.json +@@ -0,0 +1,8 @@ ++{ ++ "Logging": { ++ "LogLevel": { ++ "Default": "Information", ++ "Microsoft.AspNetCore": "Warning" ++ } ++ } ++} +diff --git a/octo-fiesta/appsettings.json b/originals/octo-fiestarr/octo-fiesta/appsettings.json +similarity index 100% +rename from octo-fiesta/appsettings.json +rename to originals/octo-fiestarr/octo-fiesta/appsettings.json +diff --git a/octo-fiesta/octo-fiesta.csproj b/originals/octo-fiestarr/octo-fiesta/octo-fiesta.csproj +similarity index 100% +rename from octo-fiesta/octo-fiesta.csproj +rename to originals/octo-fiestarr/octo-fiesta/octo-fiesta.csproj +diff --git a/octo-fiesta/octo-fiesta.http b/originals/octo-fiestarr/octo-fiesta/octo-fiesta.http +similarity index 100% +rename from octo-fiesta/octo-fiesta.http +rename to originals/octo-fiestarr/octo-fiesta/octo-fiesta.http +diff --git a/originals/octo-fiestarr/package-lock.json b/originals/octo-fiestarr/package-lock.json +new file mode 100644 +index 0000000..03f0c22 +--- /dev/null ++++ b/originals/octo-fiestarr/package-lock.json +@@ -0,0 +1,6 @@ ++{ ++ "name": "octo-fiesta", ++ "lockfileVersion": 3, ++ "requires": true, ++ "packages": {} ++} +diff --git a/package-lock.json b/package-lock.json +index 03f0c22..0ea9efd 100644 +--- a/package-lock.json ++++ b/package-lock.json +@@ -1,5 +1,5 @@ + { +- "name": "octo-fiesta", ++ "name": "allstarr", + "lockfileVersion": 3, + "requires": true, + "packages": {} diff --git a/octo-fiesta/Program.cs b/octo-fiesta/Program.cs deleted file mode 100644 index f59ae52..0000000 --- a/octo-fiesta/Program.cs +++ /dev/null @@ -1,130 +0,0 @@ -using octo_fiesta.Models.Settings; -using octo_fiesta.Services; -using octo_fiesta.Services.Deezer; -using octo_fiesta.Services.Qobuz; -using octo_fiesta.Services.SquidWTF; -using octo_fiesta.Services.Local; -using octo_fiesta.Services.Validation; -using octo_fiesta.Services.Subsonic; -using octo_fiesta.Services.Common; -using octo_fiesta.Middleware; - -var builder = WebApplication.CreateBuilder(args); - -// Add services to the container. - -builder.Services.AddControllers(); -builder.Services.AddHttpClient(); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -builder.Services.AddHttpContextAccessor(); - -// Exception handling -builder.Services.AddExceptionHandler(); -builder.Services.AddProblemDetails(); - -// Configuration -builder.Services.Configure( - builder.Configuration.GetSection("Subsonic")); -builder.Services.Configure( - builder.Configuration.GetSection("Deezer")); -builder.Services.Configure( - builder.Configuration.GetSection("Qobuz")); - -// Get the configured music service -var musicService = builder.Configuration.GetValue("Subsonic:MusicService"); -var enableExternalPlaylists = builder.Configuration.GetValue("Subsonic:EnableExternalPlaylists", true); - -// Business services -// Registered as Singleton to share state (mappings cache, scan debounce, download tracking, rate limiting) -builder.Services.AddSingleton(); - -// Subsonic services -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddScoped(); - -// Register music service based on configuration -// IMPORTANT: Primary service MUST be registered LAST because ASP.NET Core DI -// will use the last registered implementation when injecting IMusicMetadataService/IDownloadService -if (musicService == MusicService.Qobuz) -{ - // If playlists enabled, register Deezer FIRST (secondary provider) - if (enableExternalPlaylists) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - } - - // Qobuz services (primary) - registered LAST to be injected by default - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); -} -else if (musicService == MusicService.Deezer) -{ - // If playlists enabled, register Qobuz FIRST (secondary provider) - if (enableExternalPlaylists) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - } - - // Deezer services (primary, default) - registered LAST to be injected by default - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); -} -else if (musicService == MusicService.SquidWTF) -{ - // SquidWTF services - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); -} - -// Startup validation - register validators -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -// Register orchestrator as hosted service -builder.Services.AddHostedService(); - -// Register cache cleanup service (only runs when StorageMode is Cache) -builder.Services.AddHostedService(); - -builder.Services.AddCors(options => -{ - options.AddDefaultPolicy(policy => - { - policy.AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader() - .WithExposedHeaders("X-Content-Duration", "X-Total-Count", "X-Nd-Authorization"); - }); -}); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -app.UseExceptionHandler(_ => { }); // Global exception handler - -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -app.UseHttpsRedirection(); - -app.UseAuthorization(); - -app.UseCors(); - -app.MapControllers(); - -app.Run(); \ No newline at end of file diff --git a/octo-fiesta/appsettings.json b/octo-fiesta/appsettings.json deleted file mode 100644 index 95303fc..0000000 --- a/octo-fiesta/appsettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "Subsonic": { - "Url": "https://navidrome.local.bransonb.com", - "MusicService": "SquidWTF", - "ExplicitFilter": "All", - "DownloadMode": "Track", - "StorageMode": "Permanent", - "CacheDurationHours": 1 - }, - "Library": { - "DownloadPath": "./downloads" - }, - "Qobuz": { - "UserAuthToken": "your-qobuz-token", - "UserId": "your-qobuz-user-id", - "Quality": "FLAC" - }, - "Deezer": { - "Arl": "your-deezer-arl-token", - "ArlFallback": "", - "Quality": "FLAC" - }, - "SquidWTF": { - "Quality": "FLAC" - } - -} diff --git a/octo-fiesta/octo-fiesta.http b/octo-fiesta/octo-fiesta.http deleted file mode 100644 index 2f7b0c8..0000000 --- a/octo-fiesta/octo-fiesta.http +++ /dev/null @@ -1,6 +0,0 @@ -@octo_fiesta_HostAddress = http://localhost:5274 - -GET {{octo_fiesta_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/package-lock.json b/package-lock.json index 03f0c22..0ea9efd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "octo-fiesta", + "name": "allstarr", "lockfileVersion": 3, "requires": true, "packages": {}